commit
86189437e2
@ -1,31 +0,0 @@ |
||||
import { AbstractPanel } from './panel' |
||||
import * as packageJson from '../../../../../package.json' |
||||
const csjs = require('csjs-inject') |
||||
const yo = require('yo-yo') |
||||
|
||||
const css = csjs` |
||||
.pluginsContainer { |
||||
display: none; |
||||
} |
||||
` |
||||
|
||||
const profile = { |
||||
name: 'hiddenPanel', |
||||
displayName: 'Hidden Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
export class HiddenPanel extends AbstractPanel { |
||||
constructor () { |
||||
super(profile) |
||||
} |
||||
|
||||
render () { |
||||
return yo` |
||||
<div class=${css.pluginsContainer}> |
||||
${this.view} |
||||
</div>` |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
// eslint-disable-next-line no-use-before-define
|
||||
import React from 'react' |
||||
import ReactDOM from 'react-dom' // eslint-disable-line
|
||||
import { AbstractPanel } from './panel' |
||||
import * as packageJson from '../../../../../package.json' |
||||
import { RemixPluginPanel } from '@remix-ui/panel' |
||||
|
||||
const profile = { |
||||
name: 'hiddenPanel', |
||||
displayName: 'Hidden Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
export class HiddenPanel extends AbstractPanel { |
||||
el: HTMLElement |
||||
constructor () { |
||||
super(profile) |
||||
this.el = document.createElement('div') |
||||
this.el.setAttribute('class', 'pluginsContainer') |
||||
} |
||||
|
||||
addView (profile: any, view: any): void { |
||||
super.removeView(profile) |
||||
super.addView(profile, view) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
render () { |
||||
return this.el |
||||
} |
||||
|
||||
renderComponent () { |
||||
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.el) |
||||
} |
||||
} |
@ -1,38 +0,0 @@ |
||||
import { AbstractPanel } from './panel' |
||||
import * as packageJson from '../../../../../package.json' |
||||
const yo = require('yo-yo') |
||||
const csjs = require('csjs-inject') |
||||
|
||||
const css = csjs` |
||||
.pluginsContainer { |
||||
height: 100%; |
||||
display: flex; |
||||
overflow-y: hidden; |
||||
} |
||||
` |
||||
|
||||
const profile = { |
||||
name: 'mainPanel', |
||||
displayName: 'Main Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
export class MainPanel extends AbstractPanel { |
||||
constructor () { |
||||
super(profile) |
||||
} |
||||
|
||||
focus (name) { |
||||
this.emit('focusChanged', name) |
||||
super.focus(name) |
||||
} |
||||
|
||||
render () { |
||||
return yo` |
||||
<div class=${css.pluginsContainer} data-id="mainPanelPluginsContainer" id='mainPanelPluginsContainer-id'> |
||||
${this.view} |
||||
</div>` |
||||
} |
||||
} |
@ -0,0 +1,57 @@ |
||||
import React from 'react' // eslint-disable-line
|
||||
import { AbstractPanel } from './panel' |
||||
import ReactDOM from 'react-dom' // eslint-disable-line
|
||||
import { RemixPluginPanel } from '@remix-ui/panel' |
||||
import packageJson from '../../../../../package.json' |
||||
|
||||
const profile = { |
||||
name: 'mainPanel', |
||||
displayName: 'Main Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView', 'showContent'] |
||||
} |
||||
|
||||
export class MainPanel extends AbstractPanel { |
||||
element: HTMLDivElement |
||||
constructor (config) { |
||||
super(profile) |
||||
this.element = document.createElement('div') |
||||
this.element.setAttribute('data-id', 'mainPanelPluginsContainer') |
||||
this.element.setAttribute('style', 'height: 100%; width: 100%;') |
||||
// this.config = config
|
||||
} |
||||
|
||||
onActivation () { |
||||
this.renderComponent() |
||||
} |
||||
|
||||
focus (name) { |
||||
this.emit('focusChanged', name) |
||||
super.focus(name) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
super.addView(profile, view) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
removeView (profile) { |
||||
super.removeView(profile) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
async showContent (name) { |
||||
super.showContent(name) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
render () { |
||||
return this.element |
||||
} |
||||
|
||||
renderComponent () { |
||||
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.element) |
||||
} |
||||
} |
@ -1,111 +0,0 @@ |
||||
import { EventEmitter } from 'events' |
||||
import { HostPlugin } from '@remixproject/engine-web' |
||||
const csjs = require('csjs-inject') |
||||
const yo = require('yo-yo') |
||||
|
||||
const css = csjs` |
||||
.plugins { |
||||
height: 100%; |
||||
} |
||||
.plugItIn { |
||||
display : none; |
||||
height : 100%; |
||||
} |
||||
.plugItIn > div { |
||||
overflow-y : auto; |
||||
overflow-x : hidden; |
||||
height : 100%; |
||||
width : 100%; |
||||
} |
||||
.plugItIn.active { |
||||
display : block; |
||||
} |
||||
.pluginsContainer { |
||||
height : 100%; |
||||
overflow-y : hidden; |
||||
} |
||||
` |
||||
|
||||
/** Abstract class used for hosting the view of a plugin */ |
||||
export class AbstractPanel extends HostPlugin { |
||||
constructor (profile) { |
||||
super(profile) |
||||
this.events = new EventEmitter() |
||||
this.contents = {} |
||||
this.active = undefined |
||||
|
||||
// View where the plugin HTMLElement leaves
|
||||
this.view = yo`<div id="plugins" class="${css.plugins}"></div>` |
||||
} |
||||
|
||||
/** |
||||
* Add the plugin to the panel |
||||
* @param {String} name the name of the plugin |
||||
* @param {HTMLElement} content the HTMLContent of the plugin |
||||
*/ |
||||
add (view, name) { |
||||
if (this.contents[name]) throw new Error(`Plugin ${name} already rendered`) |
||||
view.style.height = '100%' |
||||
view.style.width = '100%' |
||||
view.style.border = '0' |
||||
|
||||
const isIframe = view.tagName === 'IFRAME' |
||||
view.style.display = isIframe ? 'none' : 'block' |
||||
const loading = isIframe ? yo` |
||||
<div class="d-flex justify-content-center align-items-center"> |
||||
<div class="spinner-border" role="status"> |
||||
<span class="sr-only">Loading...</span> |
||||
</div> |
||||
</div> |
||||
` : ''
|
||||
this.contents[name] = yo`<div class="${css.plugItIn}" >${view}${loading}</div>` |
||||
|
||||
if (view.tagName === 'IFRAME') { |
||||
view.addEventListener('load', () => { |
||||
if (this.contents[name].contains(loading)) { |
||||
this.contents[name].removeChild(loading) |
||||
} |
||||
view.style.display = 'block' |
||||
}) |
||||
} |
||||
this.contents[name].style.display = 'none' |
||||
this.view.appendChild(this.contents[name]) |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
this.add(view, profile.name) |
||||
} |
||||
|
||||
removeView (profile) { |
||||
this.remove(profile.name) |
||||
} |
||||
|
||||
/** |
||||
* Remove a plugin from the panel |
||||
* @param {String} name The name of the plugin to remove |
||||
*/ |
||||
remove (name) { |
||||
const el = this.contents[name] |
||||
delete this.contents[name] |
||||
if (el) el.parentElement.removeChild(el) |
||||
if (name === this.active) this.active = undefined |
||||
} |
||||
|
||||
/** |
||||
* Display the content of this specific plugin |
||||
* @param {String} name The name of the plugin to display the content |
||||
*/ |
||||
showContent (name) { |
||||
if (!this.contents[name]) throw new Error(`Plugin ${name} is not yet activated`) |
||||
// hiding the current view and display the `moduleName`
|
||||
if (this.active) { |
||||
this.contents[this.active].style.display = 'none' |
||||
} |
||||
this.contents[name].style.display = 'flex' |
||||
this.active = name |
||||
} |
||||
|
||||
focus (name) { |
||||
this.showContent(name) |
||||
} |
||||
} |
@ -0,0 +1,63 @@ |
||||
import React from 'react' // eslint-disable-line
|
||||
import { EventEmitter } from 'events' |
||||
import { HostPlugin } from '@remixproject/engine-web' // eslint-disable-line
|
||||
import { PluginRecord } from 'libs/remix-ui/panel/src/lib/types' |
||||
const EventManager = require('../../lib/events') |
||||
|
||||
export class AbstractPanel extends HostPlugin { |
||||
events: EventEmitter |
||||
event: any |
||||
public plugins: Record<string, PluginRecord> = {} |
||||
constructor (profile) { |
||||
super(profile) |
||||
this.events = new EventEmitter() |
||||
this.event = new EventManager() |
||||
} |
||||
|
||||
currentFocus (): string { |
||||
return Object.values(this.plugins).find(plugin => { |
||||
return plugin.active |
||||
}).profile.name |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
if (this.plugins[profile.name]) throw new Error(`Plugin ${profile.name} already rendered`) |
||||
this.plugins[profile.name] = { |
||||
profile: profile, |
||||
view: view, |
||||
active: false, |
||||
class: 'plugItIn active' |
||||
} |
||||
} |
||||
|
||||
removeView (profile) { |
||||
this.emit('pluginDisabled', profile.name) |
||||
this.call('menuicons', 'unlinkContent', profile) |
||||
this.remove(profile.name) |
||||
} |
||||
|
||||
/** |
||||
* Remove a plugin from the panel |
||||
* @param {String} name The name of the plugin to remove |
||||
*/ |
||||
remove (name) { |
||||
delete this.plugins[name] |
||||
} |
||||
|
||||
/** |
||||
* Display the content of this specific plugin |
||||
* @param {String} name The name of the plugin to display the content |
||||
*/ |
||||
showContent (name) { |
||||
if (!this.plugins[name]) throw new Error(`Plugin ${name} is not yet activated`) |
||||
|
||||
Object.values(this.plugins).forEach(plugin => { |
||||
plugin.active = false |
||||
}) |
||||
this.plugins[name].active = true |
||||
} |
||||
|
||||
focus (name) { |
||||
this.showContent(name) |
||||
} |
||||
} |
@ -1,156 +0,0 @@ |
||||
import { AbstractPanel } from './panel' |
||||
import * as packageJson from '../../../../../package.json' |
||||
const csjs = require('csjs-inject') |
||||
const yo = require('yo-yo') |
||||
|
||||
const css = csjs` |
||||
.panel { |
||||
width: 100%; |
||||
height: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
flex: auto; |
||||
} |
||||
.swapitTitle { |
||||
margin: 0; |
||||
text-transform: uppercase; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
.swapitTitle i{ |
||||
padding-left: 6px; |
||||
font-size: 14px; |
||||
} |
||||
.swapitHeader { |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 16px 24px 15px; |
||||
justify-content: space-between; |
||||
} |
||||
.icons i { |
||||
height: 80%; |
||||
cursor: pointer; |
||||
} |
||||
.pluginsContainer { |
||||
height: 100%; |
||||
overflow-y: auto; |
||||
} |
||||
.titleInfo { |
||||
padding-left: 10px; |
||||
} |
||||
.versionBadge { |
||||
background-color: var(--light); |
||||
padding: 0 7px; |
||||
font-weight: bolder; |
||||
margin-left: 5px; |
||||
text-transform: lowercase; |
||||
cursor: default; |
||||
} |
||||
` |
||||
|
||||
const sidePanel = { |
||||
name: 'sidePanel', |
||||
displayName: 'Side Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
// TODO merge with vertical-icons.js
|
||||
export class SidePanel extends AbstractPanel { |
||||
constructor (appManager, verticalIcons) { |
||||
super(sidePanel) |
||||
this.appManager = appManager |
||||
this.header = yo`<header></header>` |
||||
this.renderHeader() |
||||
this.verticalIcons = verticalIcons |
||||
|
||||
// Toggle content
|
||||
verticalIcons.events.on('toggleContent', (name) => { |
||||
if (!this.contents[name]) return |
||||
if (this.active === name) { |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('toggle', name) |
||||
this.events.emit('toggle', name) |
||||
return |
||||
} |
||||
this.showContent(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showing', name) |
||||
this.events.emit('showing', name) |
||||
}) |
||||
// Force opening
|
||||
verticalIcons.events.on('showContent', (name) => { |
||||
if (!this.contents[name]) return |
||||
this.showContent(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showing', name) |
||||
this.events.emit('showing', name) |
||||
}) |
||||
} |
||||
|
||||
focus (name) { |
||||
this.emit('focusChanged', name) |
||||
super.focus(name) |
||||
} |
||||
|
||||
removeView (profile) { |
||||
super.removeView(profile) |
||||
this.emit('pluginDisabled', profile.name) |
||||
this.verticalIcons.unlinkContent(profile) |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
super.addView(profile, view) |
||||
this.verticalIcons.linkContent(profile) |
||||
} |
||||
|
||||
/** |
||||
* Display content and update the header |
||||
* @param {String} name The name of the plugin to display |
||||
*/ |
||||
async showContent (name) { |
||||
super.showContent(name) |
||||
this.renderHeader() |
||||
this.emit('focusChanged', name) |
||||
} |
||||
|
||||
/** The header of the side panel */ |
||||
async renderHeader () { |
||||
let name = ' - ' |
||||
let docLink = '' |
||||
let versionWarning |
||||
if (this.active) { |
||||
const profile = await this.appManager.getProfile(this.active) |
||||
name = profile.displayName ? profile.displayName : profile.name |
||||
docLink = profile.documentation ? yo`<a href="${profile.documentation}" class="${css.titleInfo}" title="link to documentation" target="_blank"><i aria-hidden="true" class="fas fa-book"></i></a>` : '' |
||||
if (profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g)) { |
||||
versionWarning = yo`<small title="Version Alpha" class="badge-light ${css.versionBadge}">alpha</small>` |
||||
} |
||||
// Beta
|
||||
if (profile.version && profile.version.match(/\b(\w*beta\w*)\b/g)) { |
||||
versionWarning = yo`<small title="Version Beta" class="badge-light ${css.versionBadge}">beta</small>` |
||||
} |
||||
} |
||||
|
||||
const header = yo` |
||||
<header class="${css.swapitHeader}"> |
||||
<h6 class="${css.swapitTitle}" data-id="sidePanelSwapitTitle">${name}</h6> |
||||
${docLink} |
||||
${versionWarning} |
||||
</header> |
||||
` |
||||
yo.update(this.header, header) |
||||
} |
||||
|
||||
render () { |
||||
return yo` |
||||
<section class="${css.panel} plugin-manager"> |
||||
${this.header} |
||||
<div class="${css.pluginsContainer}"> |
||||
${this.view} |
||||
</div> |
||||
</section>` |
||||
} |
||||
} |
@ -0,0 +1,95 @@ |
||||
// eslint-disable-next-line no-use-before-define
|
||||
import React from 'react' |
||||
import ReactDOM from 'react-dom' |
||||
import { AbstractPanel } from './panel' |
||||
import { RemixPluginPanel } from '@remix-ui/panel' |
||||
import packageJson from '../../../../../package.json' |
||||
import { RemixAppManager } from '../../remixAppManager' |
||||
import { VerticalIcons } from 'libs/remix-ui/vertical-icons-panel/types/vertical-icons-panel' |
||||
import RemixUIPanelHeader from 'libs/remix-ui/panel/src/lib/plugins/panel-header' |
||||
// const csjs = require('csjs-inject')
|
||||
|
||||
const sidePanel = { |
||||
name: 'sidePanel', |
||||
displayName: 'Side Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
// TODO merge with vertical-icons.js
|
||||
export class SidePanel extends AbstractPanel { |
||||
appManager: RemixAppManager |
||||
sideelement: any |
||||
verticalIcons: VerticalIcons; |
||||
constructor (appManager: RemixAppManager, verticalIcons: VerticalIcons) { |
||||
super(sidePanel) |
||||
this.appManager = appManager |
||||
this.sideelement = document.createElement('section') |
||||
this.sideelement.setAttribute('class', 'panel plugin-manager') |
||||
this.verticalIcons = verticalIcons |
||||
|
||||
// Toggle content
|
||||
verticalIcons.events.on('toggleContent', (name) => { |
||||
if (!this.plugins[name]) return |
||||
if (this.plugins[name].active) { |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('toggle', name) |
||||
this.events.emit('toggle', name) |
||||
return |
||||
} |
||||
this.showContent(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showing', name) |
||||
this.events.emit('showing', name) |
||||
}) |
||||
// Force opening
|
||||
verticalIcons.events.on('showContent', (name) => { |
||||
if (!this.plugins[name]) return |
||||
this.showContent(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showing', name) |
||||
this.events.emit('showing', name) |
||||
}) |
||||
} |
||||
|
||||
onActivation () { |
||||
this.renderComponent() |
||||
} |
||||
|
||||
focus (name) { |
||||
this.emit('focusChanged', name) |
||||
super.focus(name) |
||||
} |
||||
|
||||
removeView (profile) { |
||||
super.removeView(profile) |
||||
this.emit('pluginDisabled', profile.name) |
||||
this.call('menuicons', 'unlinkContent', profile) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
super.addView(profile, view) |
||||
this.verticalIcons.linkContent(profile) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
/** |
||||
* Display content and update the header |
||||
* @param {String} name The name of the plugin to display |
||||
*/ |
||||
async showContent (name) { |
||||
super.showContent(name) |
||||
this.emit('focusChanged', name) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
render () { |
||||
return this.sideelement |
||||
} |
||||
|
||||
renderComponent () { |
||||
ReactDOM.render(<RemixPluginPanel header={<RemixUIPanelHeader plugins={this.plugins}></RemixUIPanelHeader>} plugins={this.plugins}/>, this.sideelement) |
||||
} |
||||
} |
@ -1,194 +0,0 @@ |
||||
'use strict' |
||||
import { sourceMappingDecoder } from '@remix-project/remix-debug' |
||||
import Registry from '../state/registry' |
||||
const yo = require('yo-yo') |
||||
|
||||
const css = require('./styles/contextView-styles') |
||||
|
||||
/* |
||||
Display information about the current focused code: |
||||
- if it's a reference, display information about the declaration |
||||
- jump to the declaration |
||||
- number of references |
||||
- rename declaration/references |
||||
*/ |
||||
class ContextView { |
||||
constructor (opts) { |
||||
this._components = {} |
||||
this._components.registry = Registry.getInstance() |
||||
this.contextualListener = opts.contextualListener |
||||
this.editor = opts.editor |
||||
this._deps = { |
||||
compilersArtefacts: this._components.registry.get('compilersartefacts').api, |
||||
offsetToLineColumnConverter: this._components.registry.get('offsettolinecolumnconverter').api, |
||||
config: this._components.registry.get('config').api, |
||||
fileManager: this._components.registry.get('filemanager').api |
||||
} |
||||
this._view = null |
||||
this._nodes = null |
||||
this._current = null |
||||
this.sourceMappingDecoder = sourceMappingDecoder |
||||
this.previousElement = null |
||||
this.contextualListener.event.register('contextChanged', nodes => { |
||||
this.show() |
||||
this._nodes = nodes |
||||
this.update() |
||||
}) |
||||
this.contextualListener.event.register('stopHighlighting', () => { |
||||
}) |
||||
} |
||||
|
||||
render () { |
||||
const view = yo` |
||||
<div class="${css.contextview} ${css.contextviewcontainer} bg-light text-dark border-0"> |
||||
<div class=${css.container}> |
||||
${this._renderTarget()} |
||||
</div> |
||||
</div>` |
||||
if (!this._view) { |
||||
this._view = view |
||||
} |
||||
return view |
||||
} |
||||
|
||||
hide () { |
||||
if (this._view) { |
||||
this._view.style.display = 'none' |
||||
} |
||||
} |
||||
|
||||
show () { |
||||
if (this._view) { |
||||
this._view.style.display = 'block' |
||||
} |
||||
} |
||||
|
||||
update () { |
||||
if (this._view) { |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
} |
||||
|
||||
_renderTarget () { |
||||
let last |
||||
const previous = this._current |
||||
if (this._nodes && this._nodes.length) { |
||||
last = this._nodes[this._nodes.length - 1] |
||||
if (isDefinition(last)) { |
||||
this._current = last |
||||
} else { |
||||
const target = this.contextualListener.declarationOf(last) |
||||
if (target) { |
||||
this._current = target |
||||
} else { |
||||
this._current = null |
||||
} |
||||
} |
||||
} |
||||
if (!this._current || !previous || previous.id !== this._current.id || (this.previousElement && !this.previousElement.children.length)) { |
||||
this.previousElement = this._render(this._current, last) |
||||
} |
||||
return this.previousElement |
||||
} |
||||
|
||||
_jumpToInternal (position) { |
||||
const jumpToLine = (lineColumn) => { |
||||
if (lineColumn.start && lineColumn.start.line && lineColumn.start.column) { |
||||
this.editor.gotoLine(lineColumn.start.line, lineColumn.end.column + 1) |
||||
} |
||||
} |
||||
const lastCompilationResult = this._deps.compilersArtefacts.__last |
||||
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) { |
||||
const lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn( |
||||
position, |
||||
position.file, |
||||
lastCompilationResult.getSourceCode().sources, |
||||
lastCompilationResult.getAsts()) |
||||
const filename = lastCompilationResult.getSourceName(position.file) |
||||
// TODO: refactor with rendererAPI.errorClick
|
||||
if (filename !== this._deps.config.get('currentFile')) { |
||||
const provider = this._deps.fileManager.fileProviderOf(filename) |
||||
if (provider) { |
||||
provider.exists(filename).then(exist => { |
||||
this._deps.fileManager.open(filename) |
||||
jumpToLine(lineColumn) |
||||
}).catch(error => { |
||||
if (error) return console.log(error) |
||||
}) |
||||
} |
||||
} else { |
||||
jumpToLine(lineColumn) |
||||
} |
||||
} |
||||
} |
||||
|
||||
_render (node, nodeAtCursorPosition) { |
||||
if (!node) return yo`<div></div>` |
||||
let references = this.contextualListener.referencesOf(node) |
||||
const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType |
||||
references = `${references ? references.length : '0'} reference(s)` |
||||
|
||||
let ref = 0 |
||||
const nodes = this.contextualListener.getActiveHighlights() |
||||
for (const k in nodes) { |
||||
if (nodeAtCursorPosition.id === nodes[k].nodeId) { |
||||
ref = k |
||||
break |
||||
} |
||||
} |
||||
|
||||
// JUMP BETWEEN REFERENCES
|
||||
const jump = (e) => { |
||||
e.target.dataset.action === 'next' ? ref++ : ref-- |
||||
if (ref < 0) ref = nodes.length - 1 |
||||
if (ref >= nodes.length) ref = 0 |
||||
this._jumpToInternal(nodes[ref].position) |
||||
} |
||||
|
||||
const jumpTo = () => { |
||||
if (node && node.src) { |
||||
const position = this.sourceMappingDecoder.decode(node.src) |
||||
if (position) { |
||||
this._jumpToInternal(position) |
||||
} |
||||
} |
||||
} |
||||
|
||||
const showGasEstimation = () => { |
||||
if (node.nodeType === 'FunctionDefinition') { |
||||
const result = this.contextualListener.gasEstimation(node) |
||||
const executionCost = ' Execution cost: ' + result.executionCost + ' gas' |
||||
const codeDepositCost = 'Code deposit cost: ' + result.codeDepositCost + ' gas' |
||||
const estimatedGas = result.codeDepositCost ? `${codeDepositCost}, ${executionCost}` : `${executionCost}` |
||||
return yo` |
||||
<div class=${css.gasEstimation}> |
||||
<i class="fas fa-gas-pump ${css.gasStationIcon}" title='Gas estimation'></i> |
||||
<span>${estimatedGas}</span> |
||||
</div> |
||||
` |
||||
} |
||||
} |
||||
|
||||
return yo` |
||||
<div class=${css.line}>${showGasEstimation()} |
||||
<div title=${type} class=${css.type}>${type}</div> |
||||
<div title=${node.name} class=${css.name}>${node.name}</div> |
||||
<i class="fas fa-share ${css.jump}" aria-hidden="true" onclick=${jumpTo}></i> |
||||
<span class=${css.referencesnb}>${references}</span> |
||||
<i data-action='previous' class="fas fa-chevron-up ${css.jump}" aria-hidden="true" onclick=${jump}></i> |
||||
<i data-action='next' class="fas fa-chevron-down ${css.jump}" aria-hidden="true" onclick=${jump}></i> |
||||
</div> |
||||
` |
||||
} |
||||
} |
||||
|
||||
function isDefinition (node) { |
||||
return node.nodeType === 'ContractDefinition' || |
||||
node.nodeType === 'FunctionDefinition' || |
||||
node.nodeType === 'ModifierDefinition' || |
||||
node.nodeType === 'VariableDeclaration' || |
||||
node.nodeType === 'StructDefinition' || |
||||
node.nodeType === 'EventDefinition' |
||||
} |
||||
|
||||
module.exports = ContextView |
@ -0,0 +1,94 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
import { EventEmitter } from 'events' |
||||
import QueryParams from '../../lib/query-params' |
||||
|
||||
const profile: Profile = { |
||||
name: 'layout', |
||||
description: 'layout', |
||||
methods: ['minimize'] |
||||
} |
||||
|
||||
interface panelState { |
||||
active: boolean |
||||
plugin: Plugin |
||||
minimized: boolean |
||||
} |
||||
interface panels { |
||||
tabs: panelState |
||||
editor: panelState |
||||
main: panelState |
||||
terminal: panelState |
||||
} |
||||
|
||||
export class Layout extends Plugin { |
||||
event: any |
||||
panels: panels |
||||
constructor () { |
||||
super(profile) |
||||
this.event = new EventEmitter() |
||||
} |
||||
|
||||
async onActivation (): Promise<void> { |
||||
this.on('fileManager', 'currentFileChanged', () => { |
||||
this.panels.editor.active = true |
||||
this.panels.main.active = false |
||||
this.event.emit('change', null) |
||||
}) |
||||
this.on('tabs', 'openFile', () => { |
||||
this.panels.editor.active = true |
||||
this.panels.main.active = false |
||||
this.event.emit('change', null) |
||||
}) |
||||
this.on('tabs', 'switchApp', (name: string) => { |
||||
this.call('mainPanel', 'showContent', name) |
||||
this.panels.editor.active = false |
||||
this.panels.main.active = true |
||||
this.event.emit('change', null) |
||||
}) |
||||
this.on('tabs', 'closeApp', (name: string) => { |
||||
this.panels.editor.active = true |
||||
this.panels.main.active = false |
||||
this.event.emit('change', null) |
||||
}) |
||||
this.on('tabs', 'tabCountChanged', async count => { |
||||
if (!count) await this.call('manager', 'activatePlugin', 'home') |
||||
}) |
||||
this.on('manager', 'activate', (profile: Profile) => { |
||||
switch (profile.name) { |
||||
case 'filePanel': |
||||
this.call('menuicons', 'select', 'filePanel') |
||||
break |
||||
} |
||||
}) |
||||
document.addEventListener('keypress', e => { |
||||
if (e.shiftKey && e.ctrlKey) { |
||||
if (e.code === 'KeyF') { |
||||
// Ctrl+Shift+F
|
||||
this.call('menuicons', 'select', 'filePanel') |
||||
} else if (e.code === 'KeyA') { |
||||
// Ctrl+Shift+A
|
||||
this.call('menuicons', 'select', 'pluginManager') |
||||
} else if (e.code === 'KeyS') { |
||||
// Ctrl+Shift+S
|
||||
this.call('menuicons', 'select', 'settings') |
||||
} |
||||
e.preventDefault() |
||||
} |
||||
}) |
||||
const queryParams = new QueryParams() |
||||
const params = queryParams.get() |
||||
if (params.minimizeterminal || params.embed) { |
||||
this.panels.terminal.minimized = true |
||||
this.event.emit('change', null) |
||||
} |
||||
if (params.minimizesidepanel || params.embed) { |
||||
this.event.emit('minimizesidepanel') |
||||
} |
||||
} |
||||
|
||||
minimize (name: string, minimized:boolean): void { |
||||
this.panels[name].minimized = minimized |
||||
this.event.emit('change', null) |
||||
} |
||||
} |
@ -1,204 +0,0 @@ |
||||
import Registry from '../state/registry' |
||||
|
||||
var yo = require('yo-yo') |
||||
var EventManager = require('../../lib/events') |
||||
|
||||
var { TabProxy } = require('./tab-proxy.js') |
||||
|
||||
var csjs = require('csjs-inject') |
||||
|
||||
var css = csjs` |
||||
.mainview { |
||||
display : flex; |
||||
flex-direction : column; |
||||
height : 100%; |
||||
width : 100%; |
||||
}
|
||||
` |
||||
|
||||
// @todo(#650) Extract this into two classes: MainPanel (TabsProxy + Iframe/Editor) & BottomPanel (Terminal)
|
||||
export class MainView { |
||||
constructor (contextualListener, editor, mainPanel, fileManager, appManager, terminal) { |
||||
var self = this |
||||
self.event = new EventManager() |
||||
self._view = {} |
||||
self._components = {} |
||||
self._components.registry = Registry.getInstance() |
||||
self.contextualListener = contextualListener |
||||
self.editor = editor |
||||
self.fileManager = fileManager |
||||
self.mainPanel = mainPanel |
||||
self.txListener = Registry.getInstance().get('txlistener').api |
||||
self._components.terminal = terminal |
||||
this.appManager = appManager |
||||
this.init() |
||||
} |
||||
|
||||
showApp (name) { |
||||
this.fileManager.unselectCurrentFile() |
||||
this.mainPanel.showContent(name) |
||||
this._view.editor.style.display = 'none' |
||||
this._view.mainPanel.style.display = 'block' |
||||
} |
||||
|
||||
getAppPanel () { |
||||
return this.mainPanel |
||||
} |
||||
|
||||
init () { |
||||
var self = this |
||||
self._deps = { |
||||
config: self._components.registry.get('config').api, |
||||
fileManager: self._components.registry.get('filemanager').api |
||||
} |
||||
|
||||
self.tabProxy = new TabProxy(self.fileManager, self.editor) |
||||
/* |
||||
We listen here on event from the tab component to display / hide the editor and mainpanel |
||||
depending on the content that should be displayed |
||||
*/ |
||||
self.fileManager.events.on('currentFileChanged', (file) => { |
||||
// we check upstream for "fileChanged"
|
||||
self._view.editor.style.display = 'block' |
||||
self._view.mainPanel.style.display = 'none' |
||||
}) |
||||
self.tabProxy.event.on('openFile', (file) => { |
||||
self._view.editor.style.display = 'block' |
||||
self._view.mainPanel.style.display = 'none' |
||||
}) |
||||
self.tabProxy.event.on('closeFile', (file) => { |
||||
}) |
||||
self.tabProxy.event.on('switchApp', self.showApp.bind(self)) |
||||
self.tabProxy.event.on('closeApp', (name) => { |
||||
self._view.editor.style.display = 'block' |
||||
self._view.mainPanel.style.display = 'none' |
||||
}) |
||||
self.tabProxy.event.on('tabCountChanged', (count) => { |
||||
if (!count) this.editor.displayEmptyReadOnlySession() |
||||
}) |
||||
self.data = { |
||||
_layout: { |
||||
top: { |
||||
offset: self._terminalTopOffset(), |
||||
show: true |
||||
} |
||||
} |
||||
} |
||||
|
||||
self._components.terminal.event.register('resize', delta => self._adjustLayout('top', delta)) |
||||
if (self.txListener) { |
||||
self._components.terminal.event.register('listenOnNetWork', (listenOnNetWork) => { |
||||
self.txListener.setListenOnNetwork(listenOnNetWork) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
_terminalTopOffset () { |
||||
return this._deps.config.get('terminal-top-offset') || 150 |
||||
} |
||||
|
||||
_adjustLayout (direction, delta) { |
||||
var limitUp = 0 |
||||
var limitDown = 32 |
||||
var containerHeight = window.innerHeight - limitUp // - menu bar containerHeight
|
||||
var self = this |
||||
var layout = self.data._layout[direction] |
||||
if (layout) { |
||||
if (delta === undefined) { |
||||
layout.show = !layout.show |
||||
if (layout.show) delta = layout.offset |
||||
else delta = 0 |
||||
} else { |
||||
layout.show = true |
||||
self._deps.config.set(`terminal-${direction}-offset`, delta) |
||||
layout.offset = delta |
||||
} |
||||
} |
||||
var tmp = delta - limitDown |
||||
delta = tmp > 0 ? tmp : 0 |
||||
if (direction === 'top') { |
||||
var mainPanelHeight = containerHeight - delta |
||||
mainPanelHeight = mainPanelHeight < 0 ? 0 : mainPanelHeight |
||||
self._view.editor.style.height = `${mainPanelHeight}px` |
||||
self._view.mainPanel.style.height = `${mainPanelHeight}px` |
||||
self._view.terminal.style.height = `${delta}px` // - menu bar height
|
||||
self.editor.resize((document.querySelector('#editorWrap') || {}).checked) |
||||
self._components.terminal.scroll2bottom() |
||||
} |
||||
} |
||||
|
||||
minimizeTerminal () { |
||||
this._adjustLayout('top') |
||||
} |
||||
|
||||
showTerminal (offset) { |
||||
this._adjustLayout('top', offset || this._terminalTopOffset()) |
||||
} |
||||
|
||||
getTerminal () { |
||||
return this._components.terminal |
||||
} |
||||
|
||||
getEditor () { |
||||
var self = this |
||||
return self.editor |
||||
} |
||||
|
||||
refresh () { |
||||
var self = this |
||||
self._view.tabs.onmouseenter() |
||||
} |
||||
|
||||
log (data = {}) { |
||||
var self = this |
||||
var command = self._components.terminal.commands[data.type] |
||||
if (typeof command === 'function') command(data.value) |
||||
} |
||||
|
||||
logMessage (msg) { |
||||
var self = this |
||||
self.log({ type: 'log', value: msg }) |
||||
} |
||||
|
||||
logHtmlMessage (msg) { |
||||
var self = this |
||||
self.log({ type: 'html', value: msg }) |
||||
} |
||||
|
||||
render () { |
||||
var self = this |
||||
if (self._view.mainview) return self._view.mainview |
||||
self._view.editor = self.editor.render() |
||||
self._view.editor.style.display = 'none' |
||||
self._view.mainPanel = self.mainPanel.render() |
||||
self._view.terminal = self._components.terminal.render() |
||||
|
||||
self._view.mainview = yo` |
||||
<div class=${css.mainview}> |
||||
${self.tabProxy.renderTabsbar()} |
||||
${self._view.editor} |
||||
${self._view.mainPanel} |
||||
<div class="${css.contextview} contextview"></div> |
||||
${self._view.terminal} |
||||
</div> |
||||
` |
||||
|
||||
// INIT
|
||||
self._adjustLayout('top', self.data._layout.top.offset) |
||||
|
||||
document.addEventListener('keydown', (e) => { |
||||
if (e.altKey && e.keyCode === 84) self.tabProxy.switchNextTab() // alt + t
|
||||
}) |
||||
|
||||
return self._view.mainview |
||||
} |
||||
|
||||
registerCommand (name, command, opts) { |
||||
var self = this |
||||
return self._components.terminal.registerCommand(name, command, opts) |
||||
} |
||||
|
||||
updateTerminalFilter (filter) { |
||||
this._components.terminal.updateJournal(filter) |
||||
} |
||||
} |
@ -1,53 +0,0 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
import { AlertModal } from 'libs/remix-ui/app/src/lib/remix-app/interface' |
||||
import { ModalTypes } from 'libs/remix-ui/app/src/lib/remix-app/types' |
||||
import { AppModal } from '../../../../../libs/remix-ui/app/src' |
||||
|
||||
const profile:Profile = { |
||||
name: 'testerplugin', |
||||
displayName: 'testerplugin', |
||||
description: 'testerplugin', |
||||
methods: [] |
||||
} |
||||
|
||||
export class ModalPluginTester extends Plugin { |
||||
constructor () { |
||||
super(profile) |
||||
} |
||||
|
||||
handleMessage (message: any): void { |
||||
console.log(message) |
||||
} |
||||
|
||||
onActivation (): void { |
||||
// just a modal
|
||||
let mod:AppModal = { |
||||
id: 'modal1', |
||||
title: 'test', |
||||
message: 'test', |
||||
okFn: this.handleMessage, |
||||
okLabel: 'yes', |
||||
cancelFn: null, |
||||
cancelLabel: 'no' |
||||
} |
||||
// this.call('modal', 'modal', mod)
|
||||
|
||||
// modal with callback
|
||||
mod = { ...mod, message: 'gist url', modalType: ModalTypes.prompt, defaultValue: 'prompting' } |
||||
// this.call('modal', 'modal', mod)
|
||||
|
||||
// modal with password
|
||||
mod = { ...mod, message: 'enter password to give me eth', modalType: ModalTypes.password, defaultValue: 'pass' } |
||||
// this.call('modal', 'modal', mod)
|
||||
|
||||
const al:AlertModal = { |
||||
id: 'myalert', |
||||
message: 'alert message' |
||||
} |
||||
// this.call('modal', 'alert', al)
|
||||
|
||||
// set toaster
|
||||
// this.call('modal', 'toast', 'toast message')
|
||||
} |
||||
} |
@ -1,82 +0,0 @@ |
||||
import * as packageJson from '../../../../../package.json' |
||||
import { Plugin } from '@remixproject/engine' |
||||
import Web3 from 'web3' |
||||
const yo = require('yo-yo') |
||||
const modalDialogCustom = require('../ui/modal-dialog-custom') |
||||
|
||||
const profile = { |
||||
name: 'hardhat-provider', |
||||
displayName: 'Hardhat Provider', |
||||
kind: 'provider', |
||||
description: 'Hardhat provider', |
||||
methods: ['sendAsync'], |
||||
version: packageJson.version |
||||
} |
||||
|
||||
export default class HardhatProvider extends Plugin { |
||||
constructor (blockchain) { |
||||
super(profile) |
||||
this.provider = null |
||||
this.blocked = false // used to block any call when trying to recover after a failed connection.
|
||||
this.blockchain = blockchain |
||||
} |
||||
|
||||
onDeactivation () { |
||||
this.provider = null |
||||
this.blocked = false |
||||
} |
||||
|
||||
hardhatProviderDialogBody () { |
||||
return yo` |
||||
<div class=""> |
||||
Note: To run Hardhat network node on your system, go to hardhat project folder and run command: |
||||
<div class="border p-1">npx hardhat node</div> |
||||
<br> |
||||
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a> |
||||
<br><br> |
||||
Hardhat JSON-RPC Endpoint |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
sendAsync (data) { |
||||
return new Promise((resolve, reject) => { |
||||
if (this.blocked) return reject(new Error('provider unable to connect')) |
||||
// If provider is not set, allow to open modal only when provider is trying to connect
|
||||
if (!this.provider) { |
||||
modalDialogCustom.prompt('Hardhat node request', this.hardhatProviderDialogBody(), 'http://127.0.0.1:8545', (target) => { |
||||
this.provider = new Web3.providers.HttpProvider(target) |
||||
this.sendAsyncInternal(data, resolve, reject) |
||||
}, () => { |
||||
this.sendAsyncInternal(data, resolve, reject) |
||||
}) |
||||
} else { |
||||
this.sendAsyncInternal(data, resolve, reject) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
sendAsyncInternal (data, resolve, reject) { |
||||
if (this.provider) { |
||||
// Check the case where current environment is VM on UI and it still sends RPC requests
|
||||
// This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!'
|
||||
if (this.blockchain.getProvider() !== 'Hardhat Provider' && data.method !== 'net_listening') return reject(new Error('Environment Updated !!')) |
||||
this.provider[this.provider.sendAsync ? 'sendAsync' : 'send'](data, async (error, message) => { |
||||
if (error) { |
||||
this.blocked = true |
||||
modalDialogCustom.alert('Hardhat Provider', `Error while connecting to the hardhat provider: ${error.message}`) |
||||
await this.call('udapp', 'setEnvironmentMode', { context: 'vm', fork: 'london' }) |
||||
this.provider = null |
||||
setTimeout(_ => { this.blocked = false }, 1000) // we wait 1 second for letting remix to switch to vm
|
||||
return reject(error) |
||||
} |
||||
resolve(message) |
||||
}) |
||||
} else { |
||||
const result = data.method === 'net_listening' ? 'canceled' : [] |
||||
resolve({ jsonrpc: '2.0', result: result, id: data.id }) |
||||
} |
||||
} |
||||
} |
||||
|
||||
module.exports = HardhatProvider |
@ -0,0 +1,128 @@ |
||||
import * as packageJson from '../../../../../package.json' |
||||
import { Plugin } from '@remixproject/engine' |
||||
import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app' |
||||
import React from 'react' // eslint-disable-line
|
||||
import { Blockchain } from '../../blockchain/blockchain' |
||||
import { ethers } from 'ethers' |
||||
|
||||
const profile = { |
||||
name: 'hardhat-provider', |
||||
displayName: 'Hardhat Provider', |
||||
kind: 'provider', |
||||
description: 'Hardhat provider', |
||||
methods: ['sendAsync'], |
||||
version: packageJson.version |
||||
} |
||||
|
||||
type JsonDataRequest = { |
||||
id: number, |
||||
jsonrpc: string // version
|
||||
method: string, |
||||
params: Array<any>, |
||||
} |
||||
|
||||
type JsonDataResult = { |
||||
id: number, |
||||
jsonrpc: string // version
|
||||
result: any |
||||
} |
||||
|
||||
type RejectRequest = (error: Error) => void |
||||
type SuccessRequest = (data: JsonDataResult) => void |
||||
|
||||
export class HardhatProvider extends Plugin { |
||||
provider: ethers.providers.JsonRpcProvider |
||||
blocked: boolean |
||||
blockchain: Blockchain |
||||
target: String |
||||
|
||||
constructor (blockchain) { |
||||
super(profile) |
||||
this.provider = null |
||||
this.blocked = false // used to block any call when trying to recover after a failed connection.
|
||||
this.blockchain = blockchain |
||||
} |
||||
|
||||
onDeactivation () { |
||||
this.provider = null |
||||
this.blocked = false |
||||
} |
||||
|
||||
hardhatProviderDialogBody (): JSX.Element { |
||||
return (<div> Note: To run Hardhat network node on your system, go to hardhat project folder and run command: |
||||
<div className="border p-1">npx hardhat node</div>
|
||||
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a>
|
||||
Hardhat JSON-RPC Endpoint |
||||
</div>) |
||||
} |
||||
|
||||
sendAsync (data: JsonDataRequest): Promise<any> { |
||||
return new Promise(async (resolve, reject) => { |
||||
if (this.blocked) return reject(new Error('provider unable to connect')) |
||||
// If provider is not set, allow to open modal only when provider is trying to connect
|
||||
if (!this.provider) { |
||||
let value: string |
||||
try { |
||||
value = await ((): Promise<string> => { |
||||
return new Promise((resolve, reject) => { |
||||
const modalContent: AppModal = { |
||||
id: 'harrhatprovider', |
||||
title: 'Hardhat node request', |
||||
message: this.hardhatProviderDialogBody(), |
||||
modalType: ModalTypes.prompt, |
||||
okLabel: 'OK', |
||||
cancelLabel: 'Cancel', |
||||
okFn: (value: string) => { |
||||
setTimeout(() => resolve(value), 0) |
||||
}, |
||||
cancelFn: () => { |
||||
setTimeout(() => reject(new Error('Canceled')), 0) |
||||
}, |
||||
hideFn: () => { |
||||
setTimeout(() => reject(new Error('Hide')), 0) |
||||
}, |
||||
defaultValue: 'http://127.0.0.1:8545' |
||||
} |
||||
this.call('modal', 'modal', modalContent) |
||||
}) |
||||
})() |
||||
} catch (e) { |
||||
// the modal has been canceled/hide
|
||||
return |
||||
}
|
||||
this.provider = new ethers.providers.JsonRpcProvider(value) |
||||
this.sendAsyncInternal(data, resolve, reject)
|
||||
} else { |
||||
this.sendAsyncInternal(data, resolve, reject) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> { |
||||
if (this.provider) { |
||||
// Check the case where current environment is VM on UI and it still sends RPC requests
|
||||
// This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!'
|
||||
if (this.blockchain.getProvider() !== 'Hardhat Provider' && data.method !== 'net_listening') return reject(new Error('Environment Updated !!')) |
||||
|
||||
try { |
||||
const result = await this.provider.send(data.method, data.params) |
||||
resolve({ jsonrpc: '2.0', result, id: data.id }) |
||||
} catch (error) { |
||||
this.blocked = true |
||||
const modalContent: AlertModal = { |
||||
id: 'harrhatprovider', |
||||
title: 'Hardhat Provider', |
||||
message: `Error while connecting to the hardhat provider: ${error.message}`, |
||||
} |
||||
this.call('modal', 'alert', modalContent) |
||||
await this.call('udapp', 'setEnvironmentMode', { context: 'vm', fork: 'london' }) |
||||
this.provider = null |
||||
setTimeout(_ => { this.blocked = false }, 1000) // we wait 1 second for letting remix to switch to vm
|
||||
reject(error) |
||||
} |
||||
} else { |
||||
const result = data.method === 'net_listening' ? 'canceled' : [] |
||||
resolve({ jsonrpc: '2.0', result: result, id: data.id }) |
||||
} |
||||
} |
||||
} |
@ -1,423 +0,0 @@ |
||||
import publishToStorage from '../../../publishToStorage' |
||||
const yo = require('yo-yo') |
||||
const ethJSUtil = require('ethereumjs-util') |
||||
const css = require('../styles/run-tab-styles') |
||||
const modalDialogCustom = require('../../ui/modal-dialog-custom') |
||||
const remixLib = require('@remix-project/remix-lib') |
||||
const EventManager = remixLib.EventManager |
||||
const confirmDialog = require('../../ui/confirmDialog') |
||||
const modalDialog = require('../../ui/modaldialog') |
||||
const MultiParamManager = require('../../ui/multiParamManager') |
||||
const helper = require('../../../lib/helper') |
||||
const addTooltip = require('../../ui/tooltip') |
||||
const _paq = window._paq = window._paq || [] |
||||
|
||||
class ContractDropdownUI { |
||||
constructor (blockchain, dropdownLogic, logCallback, runView) { |
||||
this.blockchain = blockchain |
||||
this.dropdownLogic = dropdownLogic |
||||
this.logCallback = logCallback |
||||
this.runView = runView |
||||
this.event = new EventManager() |
||||
|
||||
this.listenToEvents() |
||||
this.ipfsCheckedState = false |
||||
this.exEnvironment = blockchain.getProvider() |
||||
this.listenToContextChange() |
||||
this.loadType = 'other' |
||||
} |
||||
|
||||
listenToEvents () { |
||||
this.dropdownLogic.event.register('newlyCompiled', (success, data, source, compiler, compilerFullName, file) => { |
||||
if (!this.selectContractNames) return |
||||
this.selectContractNames.innerHTML = '' |
||||
if (success) { |
||||
this.dropdownLogic.getCompiledContracts(compiler, compilerFullName).forEach((contract) => { |
||||
this.selectContractNames.appendChild(yo`<option value="${contract.name}" compiler="${compilerFullName}">${contract.name} - ${contract.file}</option>`) |
||||
}) |
||||
} |
||||
this.enableAtAddress(success) |
||||
this.enableContractNames(success) |
||||
this.setInputParamsPlaceHolder() |
||||
|
||||
if (success) { |
||||
this.compFails.style.display = 'none' |
||||
} else { |
||||
this.compFails.style.display = 'block' |
||||
} |
||||
}) |
||||
} |
||||
|
||||
listenToContextChange () { |
||||
this.blockchain.event.register('networkStatus', ({ error, network }) => { |
||||
if (error) { |
||||
console.log('can\'t detect network') |
||||
return |
||||
} |
||||
this.exEnvironment = this.blockchain.getProvider() |
||||
this.networkName = network.name |
||||
this.networkId = network.id |
||||
|
||||
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`) |
||||
|
||||
// check if an already selected option exist else use default workflow
|
||||
if (savedConfig !== null) { |
||||
this.setCheckedState(savedConfig) |
||||
} else { |
||||
this.setCheckedState(this.networkName === 'Main') |
||||
} |
||||
}) |
||||
} |
||||
|
||||
setCheckedState (value) { |
||||
value = value === 'true' ? true : value === 'false' ? false : value |
||||
this.ipfsCheckedState = value |
||||
if (this.ipfsCheckbox) this.ipfsCheckbox.checked = value |
||||
} |
||||
|
||||
toggleCheckedState () { |
||||
if (this.exEnvironment === 'vm') this.networkName = 'VM' |
||||
this.ipfsCheckedState = !this.ipfsCheckedState |
||||
window.localStorage.setItem(`ipfs/${this.exEnvironment}/${this.networkName}`, this.ipfsCheckedState) |
||||
} |
||||
|
||||
enableContractNames (enable) { |
||||
if (enable) { |
||||
if (this.selectContractNames.value === '') return |
||||
this.selectContractNames.removeAttribute('disabled') |
||||
this.selectContractNames.setAttribute('title', 'Select contract for Deploy or At Address.') |
||||
} else { |
||||
this.selectContractNames.setAttribute('disabled', true) |
||||
if (this.loadType === 'sol') { |
||||
this.selectContractNames.setAttribute('title', '⚠ Select and compile *.sol file to deploy or access a contract.') |
||||
} else { |
||||
this.selectContractNames.setAttribute('title', '⚠ Selected *.abi file allows accessing contracts, select and compile *.sol file to deploy and access one.') |
||||
} |
||||
} |
||||
} |
||||
|
||||
enableAtAddress (enable) { |
||||
if (enable) { |
||||
const address = this.atAddressButtonInput.value |
||||
if (!address || !ethJSUtil.isValidAddress(address)) { |
||||
this.enableAtAddress(false) |
||||
return |
||||
} |
||||
this.atAddress.removeAttribute('disabled') |
||||
this.atAddress.setAttribute('title', 'Interact with the given contract.') |
||||
} else { |
||||
this.atAddress.setAttribute('disabled', true) |
||||
if (this.atAddressButtonInput.value === '') { |
||||
this.atAddress.setAttribute('title', '⚠ Compile *.sol file or select *.abi file & then enter the address of deployed contract.') |
||||
} else { |
||||
this.atAddress.setAttribute('title', '⚠ Compile *.sol file or select *.abi file.') |
||||
} |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
this.compFails = yo`<i title="No contract compiled yet or compilation failed. Please check the compile tab for more information." class="m-2 ml-3 fas fa-times-circle ${css.errorIcon}" ></i>` |
||||
this.atAddress = yo`<button class="${css.atAddress} btn btn-sm btn-info" id="runAndDeployAtAdressButton" onclick=${this.loadFromAddress.bind(this)}>At Address</button>` |
||||
this.atAddressButtonInput = yo`<input class="${css.input} ${css.ataddressinput} ataddressinput form-control" placeholder="Load contract from Address" title="address of contract" oninput=${this.atAddressChanged.bind(this)} />` |
||||
this.selectContractNames = yo`<select class="${css.contractNames} custom-select" disabled title="Please compile *.sol file to deploy or access a contract"></select>` |
||||
this.abiLabel = yo`<span class="py-1">ABI file selected</span>` |
||||
if (this.exEnvironment === 'vm') this.networkName = 'VM' |
||||
this.enableAtAddress(false) |
||||
this.abiLabel.style.display = 'none' |
||||
|
||||
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`) |
||||
this.ipfsCheckedState = savedConfig === 'true' ? true : false // eslint-disable-line
|
||||
|
||||
this.ipfsCheckbox = yo` |
||||
<input |
||||
id="deployAndRunPublishToIPFS" |
||||
data-id="contractDropdownIpfsCheckbox" |
||||
class="form-check-input custom-control-input" |
||||
type="checkbox" |
||||
onchange=${() => this.toggleCheckedState()} |
||||
> |
||||
` |
||||
if (this.ipfsCheckedState) this.ipfsCheckbox.checked = true |
||||
|
||||
this.deployCheckBox = yo` |
||||
<div class="d-flex py-1 align-items-center custom-control custom-checkbox"> |
||||
${this.ipfsCheckbox} |
||||
<label |
||||
for="deployAndRunPublishToIPFS" |
||||
data-id="contractDropdownIpfsCheckboxLabel" |
||||
class="m-0 form-check-label custom-control-label ${css.checkboxAlign}" |
||||
title="Publishing the source code and metadata to IPFS facilitates source code verification using Sourcify and will greatly foster contract adoption (auditing, debugging, calling it, etc...)" |
||||
> |
||||
Publish to IPFS |
||||
</label> |
||||
</div> |
||||
` |
||||
this.createPanel = yo`<div class="${css.deployDropdown}"></div>` |
||||
this.orLabel = yo`<div class="${css.orLabel} mt-2">or</div>` |
||||
|
||||
const contractNamesContainer = yo` |
||||
<div class="${css.container}" data-id="contractDropdownContainer"> |
||||
<label class="${css.settingsLabel}">Contract</label> |
||||
<div class="${css.subcontainer}"> |
||||
${this.selectContractNames} ${this.compFails} |
||||
${this.abiLabel} |
||||
</div> |
||||
<div> |
||||
${this.createPanel} |
||||
${this.orLabel} |
||||
<div class="${css.button} ${css.atAddressSect}"> |
||||
${this.atAddress} |
||||
${this.atAddressButtonInput} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
this.selectContractNames.addEventListener('change', this.setInputParamsPlaceHolder.bind(this)) |
||||
this.setInputParamsPlaceHolder() |
||||
if (!this.contractNamesContainer) { |
||||
this.contractNamesContainer = contractNamesContainer |
||||
} |
||||
return contractNamesContainer |
||||
} |
||||
|
||||
atAddressChanged (event) { |
||||
if (!this.atAddressButtonInput.value) { |
||||
this.enableAtAddress(false) |
||||
} else { |
||||
if ((this.selectContractNames && !this.selectContractNames.getAttribute('disabled') && this.loadType === 'sol') || |
||||
this.loadType === 'abi') { |
||||
this.enableAtAddress(true) |
||||
} else { |
||||
this.enableAtAddress(false) |
||||
} |
||||
} |
||||
} |
||||
|
||||
changeCurrentFile (currentFile) { |
||||
if (!this.selectContractNames) return |
||||
if (/.(.abi)$/.exec(currentFile)) { |
||||
this.createPanel.style.display = 'none' |
||||
this.orLabel.style.display = 'none' |
||||
this.compFails.style.display = 'none' |
||||
this.loadType = 'abi' |
||||
this.contractNamesContainer.style.display = 'block' |
||||
this.abiLabel.style.display = 'block' |
||||
this.abiLabel.innerHTML = currentFile |
||||
this.selectContractNames.style.display = 'none' |
||||
this.enableContractNames(true) |
||||
this.enableAtAddress(true) |
||||
} else if (/.(.sol)$/.exec(currentFile) || |
||||
/.(.vy)$/.exec(currentFile) || // vyper
|
||||
/.(.lex)$/.exec(currentFile) || // lexon
|
||||
/.(.contract)$/.exec(currentFile)) { |
||||
this.createPanel.style.display = 'block' |
||||
this.orLabel.style.display = 'block' |
||||
this.contractNamesContainer.style.display = 'block' |
||||
this.loadType = 'sol' |
||||
this.selectContractNames.style.display = 'block' |
||||
this.abiLabel.style.display = 'none' |
||||
if (this.selectContractNames.value === '') this.enableAtAddress(false) |
||||
} else { |
||||
this.loadType = 'other' |
||||
this.createPanel.style.display = 'block' |
||||
this.orLabel.style.display = 'block' |
||||
this.contractNamesContainer.style.display = 'block' |
||||
this.selectContractNames.style.display = 'block' |
||||
this.abiLabel.style.display = 'none' |
||||
if (this.selectContractNames.value === '') this.enableAtAddress(false) |
||||
} |
||||
} |
||||
|
||||
setInputParamsPlaceHolder () { |
||||
this.createPanel.innerHTML = '' |
||||
if (this.selectContractNames.selectedIndex < 0 || this.selectContractNames.children.length <= 0) { |
||||
this.createPanel.innerHTML = 'No compiled contracts' |
||||
return |
||||
} |
||||
|
||||
const selectedContract = this.getSelectedContract() |
||||
const clickCallback = async (valArray, inputsValues) => { |
||||
var selectedContract = this.getSelectedContract() |
||||
this.createInstance(selectedContract, inputsValues) |
||||
} |
||||
const createConstructorInstance = new MultiParamManager( |
||||
0, |
||||
selectedContract.getConstructorInterface(), |
||||
clickCallback, |
||||
selectedContract.getConstructorInputs(), |
||||
'Deploy', |
||||
selectedContract.bytecodeObject, |
||||
true |
||||
) |
||||
this.createPanel.appendChild(createConstructorInstance.render()) |
||||
this.createPanel.appendChild(this.deployCheckBox) |
||||
} |
||||
|
||||
getSelectedContract () { |
||||
var contract = this.selectContractNames.children[this.selectContractNames.selectedIndex] |
||||
var contractName = contract.getAttribute('value') |
||||
var compilerAtributeName = contract.getAttribute('compiler') |
||||
|
||||
return this.dropdownLogic.getSelectedContract(contractName, compilerAtributeName) |
||||
} |
||||
|
||||
async createInstance (selectedContract, args) { |
||||
if (selectedContract.bytecodeObject.length === 0) { |
||||
return modalDialogCustom.alert('This contract may be abstract, not implement an abstract parent\'s methods completely or not invoke an inherited contract\'s constructor correctly.') |
||||
} |
||||
|
||||
var continueCb = (error, continueTxExecution, cancelCb) => { |
||||
if (error) { |
||||
var msg = typeof error !== 'string' ? error.message : error |
||||
modalDialog('Gas estimation failed', yo`<div>Gas estimation errored with the following message (see below).
|
||||
The transaction execution will likely fail. Do you want to force sending? <br> |
||||
${msg} |
||||
</div>`, |
||||
{ |
||||
label: 'Send Transaction', |
||||
fn: () => { |
||||
continueTxExecution() |
||||
} |
||||
}, { |
||||
label: 'Cancel Transaction', |
||||
fn: () => { |
||||
cancelCb() |
||||
} |
||||
}) |
||||
} else { |
||||
continueTxExecution() |
||||
} |
||||
} |
||||
|
||||
const self = this |
||||
|
||||
var promptCb = (okCb, cancelCb) => { |
||||
modalDialogCustom.promptPassphrase('Passphrase requested', 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) |
||||
} |
||||
|
||||
var statusCb = (msg) => { |
||||
return this.logCallback(msg) |
||||
} |
||||
|
||||
var finalCb = (error, contractObject, address) => { |
||||
self.event.trigger('clearInstance') |
||||
|
||||
if (error) { |
||||
return this.logCallback(error) |
||||
} |
||||
self.event.trigger('newContractInstanceAdded', [contractObject, address, contractObject.name]) |
||||
|
||||
const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file) |
||||
self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data) |
||||
if (self.ipfsCheckedState) { |
||||
_paq.push(['trackEvent', 'udapp', 'DeployAndPublish', this.networkName + '_' + this.networkId]) |
||||
publishToStorage('ipfs', self.runView.fileProvider, self.runView.fileManager, selectedContract) |
||||
} else { |
||||
_paq.push(['trackEvent', 'udapp', 'DeployOnly', this.networkName + '_' + this.networkId]) |
||||
} |
||||
} |
||||
|
||||
let contractMetadata |
||||
try { |
||||
contractMetadata = await this.runView.call('compilerMetadata', 'deployMetadataOf', selectedContract.name, selectedContract.contract.file) |
||||
} catch (error) { |
||||
return statusCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error}`) |
||||
} |
||||
|
||||
const compilerContracts = this.dropdownLogic.getCompilerContracts() |
||||
const confirmationCb = this.getConfirmationCb(modalDialog, confirmDialog) |
||||
|
||||
if (selectedContract.isOverSizeLimit()) { |
||||
return modalDialog('Contract code size over limit', yo`<div>Contract creation initialization returns data with length of more than 24576 bytes. The deployment will likely fails. <br>
|
||||
More info: <a href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md" target="_blank">eip-170</a> |
||||
</div>`, |
||||
{ |
||||
label: 'Force Send', |
||||
fn: () => { |
||||
this.deployContract(selectedContract, args, contractMetadata, compilerContracts, { continueCb, promptCb, statusCb, finalCb }, confirmationCb) |
||||
} |
||||
}, { |
||||
label: 'Cancel', |
||||
fn: () => { |
||||
this.logCallback(`creation of ${selectedContract.name} canceled by user.`) |
||||
} |
||||
}) |
||||
} |
||||
this.deployContract(selectedContract, args, contractMetadata, compilerContracts, { continueCb, promptCb, statusCb, finalCb }, confirmationCb) |
||||
} |
||||
|
||||
deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) { |
||||
_paq.push(['trackEvent', 'udapp', 'DeployContractTo', this.networkName + '_' + this.networkId]) |
||||
const { statusCb } = callbacks |
||||
if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) { |
||||
return this.blockchain.deployContractAndLibraries(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) |
||||
} |
||||
if (Object.keys(selectedContract.bytecodeLinkReferences).length) statusCb(`linking ${JSON.stringify(selectedContract.bytecodeLinkReferences, null, '\t')} using ${JSON.stringify(contractMetadata.linkReferences, null, '\t')}`) |
||||
this.blockchain.deployContractWithLibrary(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) |
||||
} |
||||
|
||||
getConfirmationCb (modalDialog, confirmDialog) { |
||||
// this code is the same as in recorder.js. TODO need to be refactored out
|
||||
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => { |
||||
if (network.name !== 'Main') { |
||||
return continueTxExecution(null) |
||||
} |
||||
const amount = this.blockchain.fromWei(tx.value, true, 'ether') |
||||
const content = confirmDialog(tx, network, amount, gasEstimation, this.blockchain.determineGasFees(tx), this.blockchain.determineGasPrice.bind(this.blockchain)) |
||||
|
||||
modalDialog('Confirm transaction', content, |
||||
{ |
||||
label: 'Confirm', |
||||
fn: () => { |
||||
this.blockchain.config.setUnpersistedProperty('doNotShowTransactionConfirmationAgain', content.querySelector('input#confirmsetting').checked) |
||||
// TODO: check if this is check is still valid given the refactor
|
||||
if (!content.gasPriceStatus) { |
||||
cancelCb('Given transaction fee is not correct') |
||||
} else { |
||||
continueTxExecution(content.txFee) |
||||
} |
||||
} |
||||
}, { |
||||
label: 'Cancel', |
||||
fn: () => { |
||||
return cancelCb('Transaction canceled by user.') |
||||
} |
||||
} |
||||
) |
||||
} |
||||
|
||||
return confirmationCb |
||||
} |
||||
|
||||
loadFromAddress () { |
||||
this.event.trigger('clearInstance') |
||||
|
||||
let address = this.atAddressButtonInput.value |
||||
if (!ethJSUtil.isValidChecksumAddress(address)) { |
||||
addTooltip(yo` |
||||
<span> |
||||
It seems you are not using a checksumed address. |
||||
<br>A checksummed address is an address that contains uppercase letters, as specified in <a href="https://eips.ethereum.org/EIPS/eip-55" target="_blank">EIP-55</a>. |
||||
<br>Checksummed addresses are meant to help prevent users from sending transactions to the wrong address. |
||||
</span>`) |
||||
address = ethJSUtil.toChecksumAddress(address) |
||||
} |
||||
this.dropdownLogic.loadContractFromAddress(address, |
||||
(cb) => { |
||||
modalDialogCustom.confirm('At Address', `Do you really want to interact with ${address} using the current ABI definition?`, cb) |
||||
}, |
||||
(error, loadType, abi) => { |
||||
if (error) { |
||||
return modalDialogCustom.alert(error) |
||||
} |
||||
if (loadType === 'abi') { |
||||
return this.event.trigger('newContractABIAdded', [abi, address]) |
||||
} |
||||
var selectedContract = this.getSelectedContract() |
||||
this.event.trigger('newContractInstanceAdded', [selectedContract.object, address, this.selectContractNames.value]) |
||||
} |
||||
) |
||||
} |
||||
} |
||||
|
||||
module.exports = ContractDropdownUI |
@ -1,108 +0,0 @@ |
||||
import { CompilerAbstract } from '@remix-project/remix-solidity' |
||||
const remixLib = require('@remix-project/remix-lib') |
||||
const txHelper = remixLib.execution.txHelper |
||||
const EventManager = remixLib.EventManager |
||||
const _paq = window._paq = window._paq || [] |
||||
|
||||
class DropdownLogic { |
||||
constructor (compilersArtefacts, config, editor, runView) { |
||||
this.compilersArtefacts = compilersArtefacts |
||||
this.config = config |
||||
this.editor = editor |
||||
this.runView = runView |
||||
|
||||
this.event = new EventManager() |
||||
|
||||
this.listenToCompilationEvents() |
||||
} |
||||
|
||||
// TODO: can be moved up; the event in contractDropdown will have to refactored a method instead
|
||||
listenToCompilationEvents () { |
||||
const broadcastCompilationResult = (file, source, languageVersion, data) => { |
||||
// TODO check whether the tab is configured
|
||||
const compiler = new CompilerAbstract(languageVersion, data, source) |
||||
this.compilersArtefacts[languageVersion] = compiler |
||||
this.compilersArtefacts.__last = compiler |
||||
this.event.trigger('newlyCompiled', [true, data, source, compiler, languageVersion, file]) |
||||
} |
||||
this.runView.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
this.runView.on('vyper', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
this.runView.on('lexon', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
this.runView.on('yulp', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
this.runView.on('optimism-compiler', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
} |
||||
|
||||
loadContractFromAddress (address, confirmCb, cb) { |
||||
if (/.(.abi)$/.exec(this.config.get('currentFile'))) { |
||||
confirmCb(() => { |
||||
var abi |
||||
try { |
||||
abi = JSON.parse(this.editor.currentContent()) |
||||
} catch (e) { |
||||
return cb('Failed to parse the current file as JSON ABI.') |
||||
} |
||||
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithABI']) |
||||
cb(null, 'abi', abi) |
||||
}) |
||||
} else { |
||||
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithArtifacts']) |
||||
cb(null, 'instance') |
||||
} |
||||
} |
||||
|
||||
getCompiledContracts (compiler, compilerFullName) { |
||||
var contracts = [] |
||||
compiler.visitContracts((contract) => { |
||||
contracts.push(contract) |
||||
}) |
||||
return contracts |
||||
} |
||||
|
||||
getSelectedContract (contractName, compilerAtributeName) { |
||||
if (!contractName) return null |
||||
|
||||
var compiler = this.compilersArtefacts[compilerAtributeName] |
||||
if (!compiler) return null |
||||
|
||||
var contract = compiler.getContract(contractName) |
||||
|
||||
return { |
||||
name: contractName, |
||||
contract: contract, |
||||
compiler: compiler, |
||||
abi: contract.object.abi, |
||||
bytecodeObject: contract.object.evm.bytecode.object, |
||||
bytecodeLinkReferences: contract.object.evm.bytecode.linkReferences, |
||||
object: contract.object, |
||||
deployedBytecode: contract.object.evm.deployedBytecode, |
||||
getConstructorInterface: () => { |
||||
return txHelper.getConstructorInterface(contract.object.abi) |
||||
}, |
||||
getConstructorInputs: () => { |
||||
var constructorInteface = txHelper.getConstructorInterface(contract.object.abi) |
||||
return txHelper.inputParametersDeclarationToString(constructorInteface.inputs) |
||||
}, |
||||
isOverSizeLimit: () => { |
||||
var deployedBytecode = contract.object.evm.deployedBytecode |
||||
return (deployedBytecode && deployedBytecode.object.length / 2 > 24576) |
||||
}, |
||||
metadata: contract.object.metadata |
||||
} |
||||
} |
||||
|
||||
getCompilerContracts () { |
||||
return this.compilersArtefacts.__last.getData().contracts |
||||
} |
||||
} |
||||
|
||||
module.exports = DropdownLogic |
@ -1,160 +0,0 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
|
||||
import * as packageJson from '../../../../../../package.json' |
||||
var yo = require('yo-yo') |
||||
var remixLib = require('@remix-project/remix-lib') |
||||
var EventManager = remixLib.EventManager |
||||
var csjs = require('csjs-inject') |
||||
var css = require('../styles/run-tab-styles') |
||||
|
||||
var modalDialogCustom = require('../../ui/modal-dialog-custom') |
||||
var modalDialog = require('../../ui/modaldialog') |
||||
var confirmDialog = require('../../ui/confirmDialog') |
||||
|
||||
var helper = require('../../../lib/helper.js') |
||||
|
||||
const profile = { |
||||
name: 'recorder', |
||||
methods: ['runScenario'], |
||||
version: packageJson.version |
||||
} |
||||
|
||||
class RecorderUI extends Plugin { |
||||
constructor (blockchain, fileManager, recorder, logCallBack, config) { |
||||
super(profile) |
||||
this.fileManager = fileManager |
||||
this.blockchain = blockchain |
||||
this.recorder = recorder |
||||
this.logCallBack = logCallBack |
||||
this.config = config |
||||
this.event = new EventManager() |
||||
} |
||||
|
||||
render () { |
||||
var css2 = csjs` |
||||
.container {} |
||||
.runTxs {} |
||||
.recorder {} |
||||
` |
||||
|
||||
this.runButton = yo`<i class="fas fa-play runtransaction ${css2.runTxs} ${css.icon}" title="Run Transactions" aria-hidden="true"></i>` |
||||
this.recordButton = yo` |
||||
<i class="fas fa-save savetransaction ${css2.recorder} ${css.icon}" |
||||
onclick=${this.triggerRecordButton.bind(this)} title="Save Transactions" aria-hidden="true"> |
||||
</i>` |
||||
|
||||
this.runButton.onclick = () => { |
||||
const file = this.config.get('currentFile') |
||||
if (!file) return modalDialogCustom.alert('A scenario file has to be selected') |
||||
this.runScenario(file) |
||||
} |
||||
} |
||||
|
||||
runScenario (file) { |
||||
if (!file) return modalDialogCustom.alert('Unable to run scenerio, no specified scenario file') |
||||
var continueCb = (error, continueTxExecution, cancelCb) => { |
||||
if (error) { |
||||
var msg = typeof error !== 'string' ? error.message : error |
||||
modalDialog('Gas estimation failed', yo`<div>Gas estimation errored with the following message (see below).
|
||||
The transaction execution will likely fail. Do you want to force sending? <br> |
||||
${msg} |
||||
</div>`, |
||||
{ |
||||
label: 'Send Transaction', |
||||
fn: () => { |
||||
continueTxExecution() |
||||
} |
||||
}, { |
||||
label: 'Cancel Transaction', |
||||
fn: () => { |
||||
cancelCb() |
||||
} |
||||
}) |
||||
} else { |
||||
continueTxExecution() |
||||
} |
||||
} |
||||
|
||||
var promptCb = (okCb, cancelCb) => { |
||||
modalDialogCustom.promptPassphrase('Passphrase requested', 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) |
||||
} |
||||
|
||||
var alertCb = (msg) => { |
||||
modalDialogCustom.alert(msg) |
||||
} |
||||
|
||||
const confirmationCb = this.getConfirmationCb(modalDialog, confirmDialog) |
||||
|
||||
this.fileManager.readFile(file).then((json) => { |
||||
// TODO: there is still a UI dependency to remove here, it's still too coupled at this point to remove easily
|
||||
this.recorder.runScenario(json, continueCb, promptCb, alertCb, confirmationCb, this.logCallBack, (error, abi, address, contractName) => { |
||||
if (error) { |
||||
return modalDialogCustom.alert(error) |
||||
} |
||||
|
||||
this.event.trigger('newScenario', [abi, address, contractName]) |
||||
}) |
||||
}).catch((error) => modalDialogCustom.alert(error)) |
||||
} |
||||
|
||||
getConfirmationCb (modalDialog, confirmDialog) { |
||||
// this code is the same as in contractDropdown.js. TODO need to be refactored out
|
||||
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => { |
||||
if (network.name !== 'Main') { |
||||
return continueTxExecution(null) |
||||
} |
||||
const amount = this.blockchain.fromWei(tx.value, true, 'ether') |
||||
const content = confirmDialog(tx, network, amount, gasEstimation, this.blockchain.determineGasFees(tx), this.blockchain.determineGasPrice.bind(this.blockchain)) |
||||
|
||||
modalDialog('Confirm transaction', content, |
||||
{ |
||||
label: 'Confirm', |
||||
fn: () => { |
||||
this.config.setUnpersistedProperty('doNotShowTransactionConfirmationAgain', content.querySelector('input#confirmsetting').checked) |
||||
// TODO: check if this is check is still valid given the refactor
|
||||
if (!content.gasPriceStatus) { |
||||
cancelCb('Given transaction fee is not correct') |
||||
} else { |
||||
continueTxExecution(content.txFee) |
||||
} |
||||
} |
||||
}, { |
||||
label: 'Cancel', |
||||
fn: () => { |
||||
return cancelCb('Transaction canceled by user.') |
||||
} |
||||
} |
||||
) |
||||
} |
||||
|
||||
return confirmationCb |
||||
} |
||||
|
||||
triggerRecordButton () { |
||||
this.saveScenario( |
||||
(path, cb) => { |
||||
modalDialogCustom.prompt('Save transactions as scenario', 'Transactions will be saved in a file under ' + path, 'scenario.json', cb) |
||||
}, |
||||
(error) => { |
||||
if (error) return modalDialogCustom.alert(error) |
||||
} |
||||
) |
||||
} |
||||
|
||||
saveScenario (promptCb, cb) { |
||||
var txJSON = JSON.stringify(this.recorder.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 = RecorderUI |
@ -1,449 +0,0 @@ |
||||
import { BN } from 'ethereumjs-util' |
||||
import Registry from '../../state/registry' |
||||
const $ = require('jquery') |
||||
const yo = require('yo-yo') |
||||
const remixLib = require('@remix-project/remix-lib') |
||||
const EventManager = remixLib.EventManager |
||||
const css = require('../styles/run-tab-styles') |
||||
const copyToClipboard = require('../../ui/copy-to-clipboard') |
||||
const modalDialogCustom = require('../../ui/modal-dialog-custom') |
||||
const addTooltip = require('../../ui/tooltip') |
||||
const helper = require('../../../lib/helper.js') |
||||
|
||||
class SettingsUI { |
||||
constructor (blockchain, networkModule) { |
||||
this.blockchain = blockchain |
||||
this.event = new EventManager() |
||||
this._components = {} |
||||
|
||||
this.blockchain.event.register('transactionExecuted', (error, from, to, data, lookupOnly, txResult) => { |
||||
if (!lookupOnly) this.el.querySelector('#value').value = 0 |
||||
if (error) return |
||||
this.updateAccountBalances() |
||||
}) |
||||
this._components = { |
||||
registry: Registry.getInstance(), |
||||
networkModule: networkModule |
||||
} |
||||
this._components.registry = Registry.getInstance() |
||||
this._deps = { |
||||
config: this._components.registry.get('config').api |
||||
} |
||||
|
||||
this._deps.config.events.on('settings/personal-mode_changed', this.onPersonalChange.bind(this)) |
||||
|
||||
setInterval(() => { |
||||
this.updateAccountBalances() |
||||
}, 1000) |
||||
|
||||
this.accountListCallId = 0 |
||||
this.loadedAccounts = {} |
||||
} |
||||
|
||||
updateAccountBalances () { |
||||
if (!this.el) return |
||||
var accounts = $(this.el.querySelector('#txorigin')).children('option') |
||||
accounts.each((index, account) => { |
||||
this.blockchain.getBalanceInEther(account.value, (err, balance) => { |
||||
if (err) return |
||||
const updated = helper.shortenAddress(account.value, balance) |
||||
if (updated !== account.innerText) { // check if the balance has been updated and update UI accordingly.
|
||||
account.innerText = updated |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
validateInputKey (e) { |
||||
// preventing not numeric keys
|
||||
// preventing 000 case
|
||||
if (!helper.isNumeric(e.key) || |
||||
(e.key === '0' && !parseInt(this.el.querySelector('#value').value) && this.el.querySelector('#value').value.length > 0)) { |
||||
e.preventDefault() |
||||
e.stopImmediatePropagation() |
||||
} |
||||
} |
||||
|
||||
validateValue () { |
||||
const valueEl = this.el.querySelector('#value') |
||||
if (!valueEl.value) { |
||||
// assign 0 if given value is
|
||||
// - empty
|
||||
valueEl.value = 0 |
||||
return |
||||
} |
||||
|
||||
let v |
||||
try { |
||||
v = new BN(valueEl.value, 10) |
||||
valueEl.value = v.toString(10) |
||||
} catch (e) { |
||||
// assign 0 if given value is
|
||||
// - not valid (for ex 4345-54)
|
||||
// - contains only '0's (for ex 0000) copy past or edit
|
||||
valueEl.value = 0 |
||||
} |
||||
|
||||
// if giveen value is negative(possible with copy-pasting) set to 0
|
||||
if (v.lt(0)) valueEl.value = 0 |
||||
} |
||||
|
||||
render () { |
||||
this.netUI = yo`<span class="${css.network} badge badge-secondary"></span>` |
||||
|
||||
var environmentEl = yo` |
||||
<div class="${css.crow}"> |
||||
<label id="selectExEnv" class="${css.settingsLabel}"> |
||||
Environment |
||||
</label> |
||||
<div class="${css.environment}"> |
||||
<select id="selectExEnvOptions" data-id="settingsSelectEnvOptions" class="form-control ${css.select} custom-select"> |
||||
<option id="vm-mode-london" data-id="settingsVMLondonMode" |
||||
title="Execution environment does not connect to any node, everything is local and in memory only." |
||||
value="vm-london" name="executionContext" fork="london"> JavaScript VM (London) |
||||
</option> |
||||
<option id="vm-mode-berlin" data-id="settingsVMBerlinMode" |
||||
title="Execution environment does not connect to any node, everything is local and in memory only." |
||||
value="vm-berlin" name="executionContext" fork="berlin" > JavaScript VM (Berlin) |
||||
</option> |
||||
<option id="injected-mode" data-id="settingsInjectedMode" |
||||
title="Execution environment has been provided by Metamask or similar provider." |
||||
value="injected" name="executionContext"> Injected Web3 |
||||
</option> |
||||
<option id="web3-mode" data-id="settingsWeb3Mode" |
||||
title="Execution environment connects to node at localhost (or via IPC if available), transactions will be sent to the network and can cause loss of money or worse! |
||||
If this page is served via https and you access your node via http, it might not work. In this case, try cloning the repository and serving it via http." |
||||
value="web3" name="executionContext"> Web3 Provider |
||||
</option> |
||||
</select> |
||||
<a href="https://remix-ide.readthedocs.io/en/latest/run.html#run-setup" target="_blank"><i class="${css.infoDeployAction} ml-2 fas fa-info" title="check out docs to setup Environment"></i></a> |
||||
</div> |
||||
</div> |
||||
` |
||||
const networkEl = yo` |
||||
<div class="${css.crow}"> |
||||
<div class="${css.settingsLabel}"> |
||||
</div> |
||||
<div class="${css.environment}" data-id="settingsNetworkEnv"> |
||||
${this.netUI} |
||||
</div> |
||||
</div> |
||||
` |
||||
const accountEl = yo` |
||||
<div class="${css.crow}"> |
||||
<label class="${css.settingsLabel}"> |
||||
Account |
||||
<span id="remixRunPlusWraper" title="Create a new account" onload=${this.updatePlusButton.bind(this)}> |
||||
<i id="remixRunPlus" class="fas fa-plus-circle ${css.icon}" aria-hidden="true" onclick=${this.newAccount.bind(this)}"></i> |
||||
</span> |
||||
</label> |
||||
<div class="${css.account}"> |
||||
<select data-id="runTabSelectAccount" name="txorigin" class="form-control ${css.select} custom-select pr-4" id="txorigin"></select> |
||||
<div style="margin-left: -5px;">${copyToClipboard(() => document.querySelector('#runTabView #txorigin').value)}</div> |
||||
<i id="remixRunSignMsg" data-id="settingsRemixRunSignMsg" class="mx-1 fas fa-edit ${css.icon}" aria-hidden="true" onclick=${this.signMessage.bind(this)} title="Sign a message using this account key"></i> |
||||
</div> |
||||
</div> |
||||
` |
||||
|
||||
const gasPriceEl = yo` |
||||
<div class="${css.crow}"> |
||||
<label class="${css.settingsLabel}">Gas limit</label> |
||||
<input type="number" class="form-control ${css.gasNval} ${css.col2}" id="gasLimit" value="3000000"> |
||||
</div> |
||||
` |
||||
|
||||
const valueEl = yo` |
||||
<div class="${css.crow}"> |
||||
<label class="${css.settingsLabel}" data-id="remixDRValueLabel">Value</label> |
||||
<div class="${css.gasValueContainer}"> |
||||
<input |
||||
type="number" |
||||
min="0" |
||||
pattern="^[0-9]" |
||||
step="1" |
||||
class="form-control ${css.gasNval} ${css.col2}" |
||||
id="value" |
||||
data-id="dandrValue" |
||||
value="0" |
||||
title="Enter the value and choose the unit" |
||||
onkeypress=${(e) => this.validateInputKey(e)} |
||||
onchange=${() => this.validateValue()} |
||||
> |
||||
<select name="unit" class="form-control p-1 ${css.gasNvalUnit} ${css.col2_2} custom-select" id="unit"> |
||||
<option data-unit="wei">Wei</option> |
||||
<option data-unit="gwei">Gwei</option> |
||||
<option data-unit="finney">Finney</option> |
||||
<option data-unit="ether">Ether</option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
` |
||||
|
||||
const el = yo` |
||||
<div class="${css.settings}"> |
||||
${environmentEl} |
||||
${networkEl} |
||||
${accountEl} |
||||
${gasPriceEl} |
||||
${valueEl} |
||||
</div> |
||||
` |
||||
|
||||
var selectExEnv = environmentEl.querySelector('#selectExEnvOptions') |
||||
this.setDropdown(selectExEnv) |
||||
|
||||
this.blockchain.event.register('contextChanged', (context, silent) => { |
||||
this.setFinalContext() |
||||
}) |
||||
|
||||
this.blockchain.event.register('networkStatus', ({ error, network }) => { |
||||
if (error) { |
||||
this.netUI.innerHTML = 'can\'t detect network ' |
||||
return |
||||
} |
||||
const networkProvider = this._components.networkModule.getNetworkProvider.bind(this._components.networkModule) |
||||
this.netUI.innerHTML = (networkProvider() !== 'vm') ? `${network.name} (${network.id || '-'}) network` : '' |
||||
}) |
||||
|
||||
setInterval(() => { |
||||
this.fillAccountsList() |
||||
}, 1000) |
||||
|
||||
this.el = el |
||||
|
||||
this.fillAccountsList() |
||||
return el |
||||
} |
||||
|
||||
setDropdown (selectExEnv) { |
||||
this.selectExEnv = selectExEnv |
||||
|
||||
const addProvider = (network) => { |
||||
selectExEnv.appendChild(yo`<option
|
||||
title="provider name: ${network.name}" |
||||
value="${network.name}" |
||||
name="executionContext" |
||||
> |
||||
${network.name} |
||||
</option>`) |
||||
addTooltip(yo`<span><b>${network.name}</b> provider added</span>`) |
||||
} |
||||
|
||||
const removeProvider = (name) => { |
||||
var env = selectExEnv.querySelector(`option[value="${name}"]`) |
||||
if (env) { |
||||
selectExEnv.removeChild(env) |
||||
addTooltip(yo`<span><b>${name}</b> provider removed</span>`) |
||||
} |
||||
} |
||||
this.blockchain.event.register('addProvider', provider => addProvider(provider)) |
||||
this.blockchain.event.register('removeProvider', name => removeProvider(name)) |
||||
|
||||
selectExEnv.addEventListener('change', (event) => { |
||||
const provider = selectExEnv.options[selectExEnv.selectedIndex] |
||||
const fork = provider.getAttribute('fork') // can be undefined if connected to an external source (web3 provider / injected)
|
||||
let context = provider.value |
||||
context = context.startsWith('vm') ? 'vm' : context // context has to be 'vm', 'web3' or 'injected'
|
||||
this.setExecutionContext({ context, fork }) |
||||
}) |
||||
|
||||
selectExEnv.value = this._getProviderDropdownValue() |
||||
} |
||||
|
||||
setExecutionContext (context) { |
||||
this.blockchain.changeExecutionContext(context, () => { |
||||
modalDialogCustom.prompt('External node request', this.web3ProviderDialogBody(), 'http://127.0.0.1:8545', (target) => { |
||||
this.blockchain.setProviderFromEndpoint(target, context, (alertMsg) => { |
||||
if (alertMsg) addTooltip(alertMsg) |
||||
this.setFinalContext() |
||||
}) |
||||
}, this.setFinalContext.bind(this)) |
||||
}, (alertMsg) => { |
||||
addTooltip(alertMsg) |
||||
}, this.setFinalContext.bind(this)) |
||||
} |
||||
|
||||
web3ProviderDialogBody () { |
||||
const thePath = '<path/to/local/folder/for/test/chain>' |
||||
|
||||
return yo` |
||||
<div class=""> |
||||
Note: To use Geth & https://remix.ethereum.org, configure it to allow requests from Remix:(see <a href="https://geth.ethereum.org/docs/rpc/server" target="_blank">Geth Docs on rpc server</a>)
|
||||
<div class="border p-1">geth --http --http.corsdomain https://remix.ethereum.org</div>
|
||||
<br> |
||||
To run Remix & a local Geth test node, use this command: (see <a href="https://geth.ethereum.org/getting-started/dev-mode" target="_blank">Geth Docs on Dev mode</a>) |
||||
<div class="border p-1">geth --http --http.corsdomain="${window.origin}" --http.api web3,eth,debug,personal,net --vmdebug --datadir ${thePath} --dev console</div> |
||||
<br> |
||||
<br>
|
||||
<b>WARNING:</b> It is not safe to use the --http.corsdomain flag with a wildcard: <b>--http.corsdomain *</b> |
||||
<br> |
||||
<br>For more info: <a href="https://remix-ide.readthedocs.io/en/latest/run.html#more-about-web3-provider" target="_blank">Remix Docs on Web3 Provider</a> |
||||
<br> |
||||
<br>
|
||||
Web3 Provider Endpoint |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
/** |
||||
* generate a value used by the env dropdown list. |
||||
* @return {String} - can return 'vm-berlin, 'vm-london', 'injected' or 'web3' |
||||
*/ |
||||
_getProviderDropdownValue () { |
||||
const provider = this.blockchain.getProvider() |
||||
const fork = this.blockchain.getCurrentFork() |
||||
return provider === 'vm' ? provider + '-' + fork : provider |
||||
} |
||||
|
||||
setFinalContext () { |
||||
// set the final context. Cause it is possible that this is not the one we've originaly selected
|
||||
this.selectExEnv.value = this._getProviderDropdownValue() |
||||
this.event.trigger('clearInstance', []) |
||||
this.updatePlusButton() |
||||
} |
||||
|
||||
updatePlusButton () { |
||||
// enable/disable + button
|
||||
const plusBtn = document.getElementById('remixRunPlus') |
||||
const plusTitle = document.getElementById('remixRunPlusWraper') |
||||
switch (this.selectExEnv.value) { |
||||
case 'injected': |
||||
plusBtn.classList.add(css.disableMouseEvents) |
||||
plusTitle.title = "Unfortunately it's not possible to create an account using injected web3. Please create the account directly from your provider (i.e metamask or other of the same type)." |
||||
|
||||
break |
||||
case 'vm': |
||||
plusBtn.classList.remove(css.disableMouseEvents) |
||||
plusTitle.title = 'Create a new account' |
||||
|
||||
break |
||||
|
||||
case 'web3': |
||||
this.onPersonalChange() |
||||
|
||||
break |
||||
default: { |
||||
plusBtn.classList.add(css.disableMouseEvents) |
||||
plusTitle.title = `Unfortunately it's not possible to create an account using an external wallet (${this.selectExEnv.value}).` |
||||
} |
||||
} |
||||
} |
||||
|
||||
onPersonalChange () { |
||||
const plusBtn = document.getElementById('remixRunPlus') |
||||
const plusTitle = document.getElementById('remixRunPlusWraper') |
||||
if (!this._deps.config.get('settings/personal-mode')) { |
||||
plusBtn.classList.add(css.disableMouseEvents) |
||||
plusTitle.title = 'Creating an account is possible only in Personal mode. Please go to Settings to enable it.' |
||||
} else { |
||||
plusBtn.classList.remove(css.disableMouseEvents) |
||||
plusTitle.title = 'Create a new account' |
||||
} |
||||
} |
||||
|
||||
newAccount () { |
||||
this.blockchain.newAccount( |
||||
'', |
||||
(cb) => { |
||||
modalDialogCustom.promptPassphraseCreation((error, passphrase) => { |
||||
if (error) { |
||||
return modalDialogCustom.alert(error) |
||||
} |
||||
cb(passphrase) |
||||
}, () => {}) |
||||
}, |
||||
(error, address) => { |
||||
if (error) { |
||||
return addTooltip('Cannot create an account: ' + error) |
||||
} |
||||
addTooltip(`account ${address} created`) |
||||
} |
||||
) |
||||
} |
||||
|
||||
getSelectedAccount () { |
||||
return this.el.querySelector('#txorigin').selectedOptions[0].value |
||||
} |
||||
|
||||
getEnvironment () { |
||||
return this.blockchain.getProvider() |
||||
} |
||||
|
||||
signMessage () { |
||||
this.blockchain.getAccounts((err, accounts) => { |
||||
if (err) { |
||||
return addTooltip(`Cannot get account list: ${err}`) |
||||
} |
||||
|
||||
var signMessageDialog = { title: 'Sign a message', text: 'Enter a message to sign', inputvalue: 'Message to sign' } |
||||
var $txOrigin = this.el.querySelector('#txorigin') |
||||
if (!$txOrigin.selectedOptions[0] && (this.blockchain.isInjectedWeb3() || this.blockchain.isWeb3Provider())) { |
||||
return addTooltip('Account list is empty, please make sure the current provider is properly connected to remix') |
||||
} |
||||
|
||||
var account = $txOrigin.selectedOptions[0].value |
||||
|
||||
var promptCb = (passphrase) => { |
||||
const modal = modalDialogCustom.promptMulti(signMessageDialog, (message) => { |
||||
this.blockchain.signMessage(message, account, passphrase, (err, msgHash, signedData) => { |
||||
if (err) { |
||||
return addTooltip(err) |
||||
} |
||||
modal.hide() |
||||
modalDialogCustom.alert(yo` |
||||
<div> |
||||
<b>hash:</b><br> |
||||
<span id="remixRunSignMsgHash" data-id="settingsRemixRunSignMsgHash">${msgHash}</span> |
||||
<br><b>signature:</b><br> |
||||
<span id="remixRunSignMsgSignature" data-id="settingsRemixRunSignMsgSignature">${signedData}</span> |
||||
</div> |
||||
`)
|
||||
}) |
||||
}, false) |
||||
} |
||||
|
||||
if (this.blockchain.isWeb3Provider()) { |
||||
return modalDialogCustom.promptPassphrase( |
||||
'Passphrase to sign a message', |
||||
'Enter your passphrase for this account to sign the message', |
||||
'', |
||||
promptCb, |
||||
false |
||||
) |
||||
} |
||||
promptCb() |
||||
}) |
||||
} |
||||
|
||||
// TODO: unclear what's the goal of accountListCallId, feels like it can be simplified
|
||||
async fillAccountsList () { |
||||
this.accountListCallId++ |
||||
const callid = this.accountListCallId |
||||
const txOrigin = this.el.querySelector('#txorigin') |
||||
let accounts = [] |
||||
try { |
||||
accounts = await this.blockchain.getAccounts() |
||||
} catch (e) { |
||||
addTooltip(`Cannot get account list: ${e}`) |
||||
} |
||||
if (!accounts) accounts = [] |
||||
if (this.accountListCallId > callid) return |
||||
this.accountListCallId++ |
||||
for (const loadedaddress in this.loadedAccounts) { |
||||
if (accounts.indexOf(loadedaddress) === -1) { |
||||
txOrigin.removeChild(txOrigin.querySelector('option[value="' + loadedaddress + '"]')) |
||||
delete this.loadedAccounts[loadedaddress] |
||||
} |
||||
} |
||||
for (const i in accounts) { |
||||
const address = accounts[i] |
||||
if (!this.loadedAccounts[address]) { |
||||
txOrigin.appendChild(yo`<option value="${address}" >${address}</option>`) |
||||
this.loadedAccounts[address] = 1 |
||||
} |
||||
} |
||||
txOrigin.setAttribute('value', accounts[0]) |
||||
} |
||||
} |
||||
|
||||
module.exports = SettingsUI |
@ -1,225 +0,0 @@ |
||||
var csjs = require('csjs-inject') |
||||
|
||||
var css = csjs` |
||||
.runTabView { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
.runTabView::-webkit-scrollbar { |
||||
display: none; |
||||
} |
||||
.settings { |
||||
padding: 0 24px 16px; |
||||
} |
||||
.crow { |
||||
display: block; |
||||
margin-top: 8px; |
||||
} |
||||
.col1 { |
||||
width: 30%; |
||||
float: left; |
||||
align-self: center; |
||||
} |
||||
.settingsLabel { |
||||
font-size: 11px; |
||||
margin-bottom: 4px; |
||||
text-transform: uppercase; |
||||
} |
||||
.environment { |
||||
display: flex; |
||||
align-items: center; |
||||
position: relative; |
||||
width: 100%; |
||||
} |
||||
.environment a { |
||||
margin-left: 7px; |
||||
} |
||||
.account { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.account i { |
||||
margin-left: 12px; |
||||
} |
||||
.col2 { |
||||
border-radius: 3px; |
||||
} |
||||
.col2_1 { |
||||
width: 164px; |
||||
min-width: 164px; |
||||
} |
||||
.col2_2 { |
||||
} |
||||
.select { |
||||
font-weight: normal; |
||||
width: 100%; |
||||
overflow: hidden; |
||||
} |
||||
.instanceContainer { |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-bottom: 2%; |
||||
border: none; |
||||
text-align: center; |
||||
padding: 0 14px 16px; |
||||
} |
||||
.pendingTxsContainer { |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-top: 2%; |
||||
border: none; |
||||
text-align: center; |
||||
} |
||||
.container { |
||||
padding: 0 24px 16px; |
||||
} |
||||
.recorderDescription { |
||||
margin: 0 15px 15px 0; |
||||
} |
||||
.contractNames { |
||||
width: 100%; |
||||
border: 1px solid |
||||
} |
||||
.subcontainer { |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
margin-bottom: 8px; |
||||
} |
||||
.subcontainer i { |
||||
width: 16px; |
||||
display: flex; |
||||
justify-content: center; |
||||
margin-left: 1px; |
||||
} |
||||
.button button{ |
||||
flex: none; |
||||
} |
||||
.button { |
||||
display: flex; |
||||
align-items: center; |
||||
margin-top: 13px; |
||||
} |
||||
.transaction { |
||||
} |
||||
.atAddress { |
||||
margin: 0; |
||||
min-width: 100px; |
||||
width: 100px; |
||||
height: 100%; |
||||
word-break: inherit; |
||||
border-top-right-radius: 0; |
||||
border-bottom-right-radius: 0; |
||||
border-right: 0; |
||||
} |
||||
.atAddressSect { |
||||
margin-top: 8px; |
||||
height: 32px; |
||||
} |
||||
.atAddressSect input { |
||||
height: 32px; |
||||
border-top-left-radius: 0 !important; |
||||
border-bottom-left-radius: 0 !important; |
||||
} |
||||
.ataddressinput { |
||||
padding: .25rem; |
||||
} |
||||
.create { |
||||
} |
||||
.input { |
||||
font-size: 10px !important; |
||||
} |
||||
.noInstancesText { |
||||
font-style: italic; |
||||
text-align: left; |
||||
padding-left: 15px; |
||||
} |
||||
.pendingTxsText { |
||||
font-style: italic; |
||||
display: flex; |
||||
justify-content: space-evenly; |
||||
align-items: center; |
||||
flex-wrap: wrap; |
||||
} |
||||
.item { |
||||
margin-right: 1em; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.pendingContainer { |
||||
display: flex; |
||||
align-items: baseline; |
||||
} |
||||
.pending { |
||||
height: 25px; |
||||
text-align: center; |
||||
padding-left: 10px; |
||||
border-radius: 3px; |
||||
margin-left: 5px; |
||||
} |
||||
.disableMouseEvents { |
||||
pointer-events: none; |
||||
} |
||||
.icon { |
||||
cursor: pointer; |
||||
font-size: 12px; |
||||
cursor: pointer; |
||||
margin-left: 5px; |
||||
} |
||||
.icon:hover { |
||||
font-size: 12px; |
||||
color: var(--warning); |
||||
} |
||||
.errorIcon { |
||||
color: var(--warning); |
||||
margin-left: 15px; |
||||
} |
||||
.failDesc { |
||||
color: var(--warning); |
||||
padding-left: 10px; |
||||
display: inline; |
||||
} |
||||
.network { |
||||
margin-left: 8px; |
||||
pointer-events: none; |
||||
} |
||||
.networkItem { |
||||
margin-right: 5px; |
||||
} |
||||
.transactionActions { |
||||
display: flex; |
||||
justify-content: space-evenly; |
||||
width: 145px; |
||||
} |
||||
.orLabel { |
||||
text-align: center; |
||||
text-transform: uppercase; |
||||
} |
||||
.infoDeployAction { |
||||
margin-left: 1px; |
||||
font-size: 13px; |
||||
color: var(--info); |
||||
} |
||||
.gasValueContainer { |
||||
flex-direction: row; |
||||
display: flex; |
||||
} |
||||
.gasNval { |
||||
width: 55%; |
||||
font-size: 0.8rem; |
||||
} |
||||
.gasNvalUnit { |
||||
width: 41%; |
||||
margin-left: 10px; |
||||
font-size: 0.8rem; |
||||
} |
||||
.deployDropdown { |
||||
text-align: center; |
||||
text-transform: uppercase; |
||||
} |
||||
.checkboxAlign { |
||||
padding-top: 2px; |
||||
} |
||||
` |
||||
|
||||
module.exports = css |
@ -1,212 +0,0 @@ |
||||
'use strict' |
||||
var yo = require('yo-yo') |
||||
var csjs = require('csjs-inject') |
||||
var css = csjs` |
||||
.li_tv { |
||||
list-style-type: none; |
||||
-webkit-margin-before: 0px; |
||||
-webkit-margin-after: 0px; |
||||
-webkit-margin-start: 0px; |
||||
-webkit-margin-end: 0px; |
||||
-webkit-padding-start: 0px; |
||||
} |
||||
.ul_tv { |
||||
list-style-type: none; |
||||
-webkit-margin-before: 0px; |
||||
-webkit-margin-after: 0px; |
||||
-webkit-margin-start: 0px; |
||||
-webkit-margin-end: 0px; |
||||
-webkit-padding-start: 0px; |
||||
} |
||||
.caret_tv { |
||||
width: 10px; |
||||
flex-shrink: 0; |
||||
padding-right: 5px; |
||||
} |
||||
.label_item { |
||||
word-break: break-all; |
||||
} |
||||
.label_key { |
||||
min-width: max-content; |
||||
max-width: 80%; |
||||
word-break: break-word; |
||||
} |
||||
.label_value { |
||||
min-width: 10%; |
||||
} |
||||
.cursor_pointer { |
||||
cursor: pointer; |
||||
} |
||||
` |
||||
|
||||
var EventManager = require('../../lib/events') |
||||
|
||||
/** |
||||
* TreeView |
||||
* - extendable by specifying custom `extractData` and `formatSelf` function |
||||
* - trigger `nodeClick` and `leafClick` |
||||
*/ |
||||
class TreeView { |
||||
constructor (opts) { |
||||
this.event = new EventManager() |
||||
this.extractData = opts.extractData || this.extractDataDefault |
||||
this.formatSelf = opts.formatSelf || this.formatSelfDefault |
||||
this.loadMore = opts.loadMore |
||||
this.view = null |
||||
this.expandPath = [] |
||||
} |
||||
|
||||
render (json, expand) { |
||||
var view = this.renderProperties(json, expand) |
||||
if (!this.view) { |
||||
this.view = view |
||||
} |
||||
return view |
||||
} |
||||
|
||||
update (json) { |
||||
if (this.view) { |
||||
yo.update(this.view, this.render(json)) |
||||
} |
||||
} |
||||
|
||||
renderObject (item, parent, key, expand, keyPath) { |
||||
var data = this.extractData(item, parent, key) |
||||
var children = (data.children || []).map((child, index) => { |
||||
return this.renderObject(child.value, data, child.key, expand, keyPath + '/' + child.key) |
||||
}) |
||||
return this.formatData(key, data, children, expand, keyPath) |
||||
} |
||||
|
||||
renderProperties (json, expand, key) { |
||||
key = key || '' |
||||
var children = Object.keys(json).map((innerkey) => { |
||||
return this.renderObject(json[innerkey], json, innerkey, expand, innerkey) |
||||
}) |
||||
return yo`<ul key=${key} data-id="treeViewUl${key}" class="${css.ul_tv} ml-0 px-2">${children}</ul>` |
||||
} |
||||
|
||||
formatData (key, data, children, expand, keyPath) { |
||||
var self = this |
||||
var li = yo`<li key=${keyPath} data-id="treeViewLi${keyPath}" class=${css.li_tv}></li>` |
||||
var caret = yo`<div class="px-1 fas fa-caret-right caret ${css.caret_tv}"></div>` |
||||
var label = yo` |
||||
<div key=${keyPath} data-id="treeViewDiv${keyPath}" class="d-flex flex-row align-items-center"> |
||||
${caret} |
||||
<span class="w-100">${self.formatSelf(key, data, li)}</span> |
||||
</div>` |
||||
const expanded = self.expandPath.includes(keyPath) |
||||
li.appendChild(label) |
||||
if (data.children) { |
||||
var list = yo`<ul key=${keyPath} data-id="treeViewUlList${keyPath}" class="pl-2 ${css.ul_tv}">${children}</ul>` |
||||
list.style.display = expanded ? 'block' : 'none' |
||||
caret.className = list.style.display === 'none' ? `fas fa-caret-right caret ${css.caret_tv}` : `fas fa-caret-down caret ${css.caret_tv}` |
||||
caret.setAttribute('data-id', `treeViewToggle${keyPath}`) |
||||
label.onclick = function () { |
||||
self.expand(keyPath) |
||||
if (self.isExpanded(keyPath)) { |
||||
if (!self.expandPath.includes(keyPath)) self.expandPath.push(keyPath) |
||||
} else { |
||||
self.expandPath = self.expandPath.filter(path => !path.startsWith(keyPath)) |
||||
} |
||||
} |
||||
label.oncontextmenu = function (event) { |
||||
self.event.trigger('nodeRightClick', [keyPath, data, label, event]) |
||||
} |
||||
li.appendChild(list) |
||||
if (data.hasNext) { |
||||
list.appendChild(yo`<li><span class="w-100 text-primary ${css.cursor_pointer}" data-id="treeViewLoadMore" onclick="${() => self.loadMore(data.cursor)}">Load more</span></li>`) |
||||
} |
||||
} else { |
||||
caret.style.visibility = 'hidden' |
||||
label.oncontextmenu = function (event) { |
||||
self.event.trigger('leafRightClick', [keyPath, data, label, event]) |
||||
} |
||||
label.onclick = function (event) { |
||||
self.event.trigger('leafClick', [keyPath, data, label, event]) |
||||
} |
||||
} |
||||
return li |
||||
} |
||||
|
||||
isExpanded (path) { |
||||
var current = this.nodeAt(path) |
||||
if (current) { |
||||
return current.style.display !== 'none' |
||||
} |
||||
return false |
||||
} |
||||
|
||||
expand (path) { |
||||
var caret = this.caretAt(path) |
||||
var node = this.nodeAt(path) |
||||
if (node) { |
||||
node.style.display = node.style.display === 'none' ? 'block' : 'none' |
||||
caret.className = node.style.display === 'none' ? `fas fa-caret-right caret ${css.caret_tv}` : `fas fa-caret-down caret ${css.caret_tv}` |
||||
this.event.trigger('nodeClick', [path, node]) |
||||
} |
||||
} |
||||
|
||||
caretAt (path) { |
||||
var label = this.labelAt(path) |
||||
if (label) { |
||||
return label.querySelector('.caret') |
||||
} |
||||
} |
||||
|
||||
itemAt (path) { |
||||
return this.view.querySelector(`li[key="${path}"]`) |
||||
} |
||||
|
||||
labelAt (path) { |
||||
return this.view.querySelector(`div[key="${path}"]`) |
||||
} |
||||
|
||||
nodeAt (path) { |
||||
return this.view.querySelector(`ul[key="${path}"]`) |
||||
} |
||||
|
||||
updateNodeFromJSON (path, jsonTree, expand) { |
||||
var newTree = this.renderProperties(jsonTree, expand, path) |
||||
var current = this.nodeAt(path) |
||||
if (current && current.parentElement) { |
||||
current.parentElement.replaceChild(newTree, current) |
||||
} |
||||
} |
||||
|
||||
formatSelfDefault (key, data) { |
||||
return yo` |
||||
<div class="d-flex mt-2 flex-row ${css.label_item}"> |
||||
<label class="small font-weight-bold pr-1 ${css.label_key}">${key}:</label> |
||||
<label class="m-0 ${css.label_value}">${data.self}</label> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
extractDataDefault (item, parent, key) { |
||||
var ret = {} |
||||
if (item instanceof Array) { |
||||
ret.children = item.map((item, index) => { |
||||
return { key: index, value: item } |
||||
}) |
||||
ret.self = 'Array' |
||||
ret.isNode = true |
||||
ret.isLeaf = false |
||||
} else if (item instanceof Object) { |
||||
ret.children = Object.keys(item).map((key) => { |
||||
return { key: key, value: item[key] } |
||||
}) |
||||
ret.self = 'Object' |
||||
ret.isNode = true |
||||
ret.isLeaf = false |
||||
} else { |
||||
ret.self = item |
||||
ret.children = null |
||||
ret.isNode = false |
||||
ret.isLeaf = true |
||||
} |
||||
return ret |
||||
} |
||||
} |
||||
|
||||
module.exports = TreeView |
@ -1,212 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var remixLib = require('@remix-project/remix-lib') |
||||
var EventManager = remixLib.EventManager |
||||
var Commands = require('../../lib/commands') |
||||
|
||||
// -------------- styling ----------------------
|
||||
var css = require('./styles/auto-complete-popup-styles') |
||||
|
||||
/* USAGE: |
||||
|
||||
var autoCompletePopup = new AutoCompletePopup({ |
||||
options: [] |
||||
}) |
||||
autoCompletePopup.event.register('handleSelect', function (input) { }) |
||||
autoCompletePopup.event.register('updateList', function () { }) |
||||
|
||||
*/ |
||||
|
||||
class AutoCompletePopup { |
||||
constructor (opts = {}) { |
||||
var self = this |
||||
self.event = new EventManager() |
||||
self.isOpen = false |
||||
self.opts = opts |
||||
self.data = { |
||||
_options: [] |
||||
} |
||||
self._components = {} |
||||
self._view = null |
||||
self._startingElement = 0 |
||||
self._elementsToShow = 4 |
||||
self._selectedElement = 0 |
||||
this.extraCommands = [] |
||||
} |
||||
|
||||
render () { |
||||
var self = this |
||||
const autoComplete = yo` |
||||
<div class="${css.popup} alert alert-secondary"> |
||||
<div> |
||||
${self.data._options.map((item, index) => { |
||||
return yo` |
||||
<div data-id="autoCompletePopUpAutoCompleteItem" class="${css.autoCompleteItem} ${css.listHandlerHide} item ${self._selectedElement === index ? 'border border-primary' : ''}"> |
||||
<div value=${index} onclick=${(event) => { self.handleSelect(event.srcElement.innerText) }}> |
||||
${getKeyOf(item)}
|
||||
</div> |
||||
<div> |
||||
${getValueOf(item)} |
||||
</div> |
||||
</div> |
||||
` |
||||
})} |
||||
</div> |
||||
<div class="${css.listHandlerHide}"> |
||||
<div class="${css.pageNumberAlignment}">Page ${(self._startingElement / self._elementsToShow) + 1} of ${Math.ceil(self.data._options.length / self._elementsToShow)}</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
function setUpPopUp (autoComplete) { |
||||
handleOpenPopup(autoComplete) |
||||
handleListSize(autoComplete) |
||||
} |
||||
|
||||
function handleOpenPopup (autoComplete) { |
||||
autoComplete.style.display = self.data._options.length > 0 ? 'block' : 'none' |
||||
} |
||||
|
||||
function handleListSize (autoComplete) { |
||||
if (self.data._options.length >= self._startingElement) { |
||||
for (let i = self._startingElement; i < (self._elementsToShow + self._startingElement); i++) { |
||||
const el = autoComplete.querySelectorAll('.item')[i] |
||||
if (el) { |
||||
el.classList.remove(css.listHandlerHide) |
||||
el.classList.add(css.listHandlerShow) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
setUpPopUp(autoComplete) |
||||
if (!this._view) this._view = autoComplete |
||||
return autoComplete |
||||
} |
||||
|
||||
handleSelect (text) { |
||||
this.removeAutoComplete() |
||||
this.event.trigger('handleSelect', [text]) |
||||
} |
||||
|
||||
moveUp () { |
||||
if (this._selectedElement === 0) return |
||||
this._selectedElement-- |
||||
this._startingElement = this._selectedElement > 0 ? this._selectedElement - 1 : 0 |
||||
this.event.trigger('updateList') |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
|
||||
moveDown () { |
||||
if (this.data._options.length <= this._selectedElement + 1) return |
||||
this._selectedElement++ |
||||
this._startingElement = this._selectedElement - 1 |
||||
this.event.trigger('updateList') |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
|
||||
handleAutoComplete (event, inputString) { |
||||
if (this.isOpen && (event.which === 27 || event.which === 8 || event.which === 46)) { |
||||
// backspace or any key that should remove the autocompletion
|
||||
this.removeAutoComplete() |
||||
return true |
||||
} |
||||
if (this.isOpen && (event.which === 13 || event.which === 9)) { |
||||
// enter and tab (validate completion)
|
||||
event.preventDefault() |
||||
if (this.data._options[this._selectedElement]) { |
||||
this.handleSelect(getKeyOf(this.data._options[this._selectedElement])) |
||||
} |
||||
this.removeAutoComplete() |
||||
return true |
||||
} |
||||
if (this.isOpen && event.which === 38) { |
||||
// move up
|
||||
event.preventDefault() |
||||
this.isOpen = true |
||||
this.moveUp() |
||||
return true |
||||
} |
||||
if (this.isOpen && event.which === 40) { |
||||
// move down
|
||||
event.preventDefault() |
||||
this.isOpen = true |
||||
this.moveDown() |
||||
return true |
||||
} |
||||
if (event.which === 13 || event.which === 9) { |
||||
// enter || tab and autocompletion is off, just returning false
|
||||
return false |
||||
} |
||||
const textList = inputString.split(' ') |
||||
const autoCompleteInput = textList.length > 1 ? textList[textList.length - 1] : textList[0] |
||||
if (inputString.length >= 2) { |
||||
// more than 2 letters, start completion
|
||||
this.data._options = [] |
||||
Commands.allPrograms.forEach(item => { |
||||
const program = getKeyOf(item) |
||||
if (program.substring(0, program.length - 1).includes(autoCompleteInput.trim())) { |
||||
this.data._options.push(item) |
||||
} else if (autoCompleteInput.trim().includes(program) || (program === autoCompleteInput.trim())) { |
||||
Commands.allCommands.forEach(item => { |
||||
const command = getKeyOf(item) |
||||
if (command.includes(autoCompleteInput.trim())) { |
||||
this.data._options.push(item) |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
this.extraCommands.forEach(item => { |
||||
const command = getKeyOf(item) |
||||
if (command.includes(autoCompleteInput.trim())) { |
||||
this.data._options.push(item) |
||||
} |
||||
}) |
||||
|
||||
if (this.data._options.length === 1 && event.which === 9) { |
||||
// if only one option and tab is pressed, we resolve it
|
||||
event.preventDefault() |
||||
textList.pop() |
||||
textList.push(getKeyOf(this.data._options[0])) |
||||
this.handleSelect(`${textList}`.replace(/,/g, ' ')) |
||||
this.removeAutoComplete() |
||||
return |
||||
} |
||||
if (this.data._options.length) this.isOpen = true |
||||
yo.update(this._view, this.render()) |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
removeAutoComplete () { |
||||
if (!this.isOpen) return |
||||
this._view.style.display = 'none' |
||||
this.isOpen = false |
||||
this.data._options = [] |
||||
this._startingElement = 0 |
||||
this._selectedElement = 0 |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
|
||||
extendAutocompletion () { |
||||
// TODO: this is not using the appManager interface. Terminal should be put as module
|
||||
this.opts.appManager.event.on('activate', async (profile) => { |
||||
if (!profile.methods) return |
||||
profile.methods.forEach((method) => { |
||||
const key = `remix.call('${profile.name}', '${method}')` |
||||
const keyValue = {} |
||||
keyValue[key] = `call ${profile.name} - ${method}` |
||||
if (this.extraCommands.includes(keyValue)) return |
||||
this.extraCommands.push(keyValue) |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function getKeyOf (item) { |
||||
return Object.keys(item)[0] |
||||
} |
||||
|
||||
function getValueOf (item) { |
||||
return Object.values(item)[0] |
||||
} |
||||
|
||||
module.exports = AutoCompletePopup |
@ -1,70 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var csjs = require('csjs-inject') |
||||
var EventManager = require('../../lib/events') |
||||
|
||||
module.exports = class Card { |
||||
constructor (api, events, opts) { |
||||
const self = this |
||||
self._api = api |
||||
self._events = events |
||||
self._opts = opts |
||||
self._view = {} |
||||
self.event = new EventManager() |
||||
} |
||||
|
||||
render () { |
||||
const self = this |
||||
if (self._view.el) return self._view.el |
||||
|
||||
self._view.cardBody = yo`<div></div>` |
||||
self._view.arrow = yo`<i class="${css.arrow} fas fa-angle-down" onclick="${() => trigger(this)}"></i>` |
||||
|
||||
self._view.expandCollapseButton = yo` |
||||
<div>${self._view.arrow}</div>` |
||||
|
||||
self._view.statusBar = yo`<div>${self._opts.collapsedView}</div>` |
||||
self._view.cardHeader = yo` |
||||
<div class="d-flex justify-content-between align-items-center" onclick=${() => trigger(self._view.arrow)}> |
||||
<div class="pr-1 d-flex flex-row"> |
||||
<div>${self._opts.title}</div> |
||||
${self._view.statusBar} |
||||
</div> |
||||
<div>${self._view.expandCollapseButton}</div> |
||||
</div>` |
||||
|
||||
function trigger (el) { |
||||
var body = self._view.cardBody |
||||
var status = self._view.statusBar |
||||
if (el.classList) { |
||||
el.classList.toggle('fa-angle-up') |
||||
var arrow = el.classList.toggle('fa-angle-down') ? 'up' : 'down' |
||||
self.event.trigger('expandCollapseCard', [arrow, body, status]) |
||||
} |
||||
} |
||||
|
||||
// HTML
|
||||
self._view.el = yo` |
||||
<div class="${css.cardContainer} list-group-item border-0"> |
||||
${self._view.cardHeader} |
||||
${self._view.cardBody} |
||||
</div>` |
||||
|
||||
return self._view.el |
||||
} |
||||
} |
||||
|
||||
const css = csjs` |
||||
.cardContainer { |
||||
padding : 0 24px 16px; |
||||
margin : 0; |
||||
background : none; |
||||
} |
||||
.arrow { |
||||
font-weight : bold; |
||||
cursor : pointer; |
||||
font-size : 14px; |
||||
} |
||||
.arrow:hover { |
||||
} |
||||
|
||||
` |
@ -1,142 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var csjs = require('csjs-inject') |
||||
const copyToClipboard = require('./copy-to-clipboard') |
||||
const Web3 = require('web3') |
||||
|
||||
var css = csjs` |
||||
#confirmsetting { |
||||
z-index: 1; |
||||
} |
||||
.txInfoBox { |
||||
} |
||||
.wrapword { |
||||
white-space: pre-wrap; /* Since CSS 2.1 */ |
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ |
||||
white-space: -pre-wrap; /* Opera 4-6 */ |
||||
white-space: -o-pre-wrap; /* Opera 7 */ |
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */ |
||||
} |
||||
` |
||||
|
||||
function confirmDialog (tx, network, amount, gasEstimation, newGasPriceCb, initialParamsCb) { |
||||
const onGasPriceChange = function () { |
||||
var gasPrice = el.querySelector('#gasprice').value |
||||
newGasPriceCb(gasPrice, (txFeeText, priceStatus) => { |
||||
el.querySelector('#txfee').innerHTML = txFeeText |
||||
el.gasPriceStatus = priceStatus |
||||
el.txFee = { gasPrice } |
||||
}) |
||||
} |
||||
|
||||
const onMaxFeeChange = function () { |
||||
var maxFee = el.querySelector('#maxfee').value |
||||
var confirmBtn = document.querySelector('#modal-footer-ok') |
||||
var maxPriorityFee = el.querySelector('#maxpriorityfee').value |
||||
if (parseInt(network.lastBlock.baseFeePerGas, 16) > Web3.utils.toWei(maxFee, 'Gwei')) { |
||||
el.querySelector('#txfee').innerHTML = 'Transaction is invalid. Max fee should not be less than Base fee' |
||||
el.gasPriceStatus = false |
||||
confirmBtn.hidden = true |
||||
return |
||||
} else { |
||||
el.gasPriceStatus = true |
||||
confirmBtn.hidden = false |
||||
} |
||||
|
||||
newGasPriceCb(maxFee, (txFeeText, priceStatus) => { |
||||
el.querySelector('#txfee').innerHTML = txFeeText |
||||
if (priceStatus) { |
||||
confirmBtn.hidden = false |
||||
} else { |
||||
confirmBtn.hidden = true |
||||
} |
||||
el.gasPriceStatus = priceStatus |
||||
el.txFee = { maxFee, maxPriorityFee, baseFeePerGas: network.lastBlock.baseFeePerGas } |
||||
}) |
||||
} |
||||
|
||||
const el = yo` |
||||
<div> |
||||
<div class="text-dark">You are about to create a transaction on ${network.name} Network. Confirm the details to send the info to your provider. |
||||
<br>The provider for many users is MetaMask. The provider will ask you to sign the transaction before it is sent to ${network.name} Network. |
||||
</div> |
||||
<div class="mt-3 ${css.txInfoBox}"> |
||||
<div> |
||||
<span class="text-dark mr-2">From:</span> |
||||
<span>${tx.from}</span> |
||||
</div> |
||||
<div> |
||||
<span class="text-dark mr-2">To:</span> |
||||
<span>${tx.to ? tx.to : '(Contract Creation)'}</span> |
||||
</div> |
||||
<div class="d-flex align-items-center"> |
||||
<span class="text-dark mr-2">Data:</span> |
||||
<pre class="${css.wrapword} mb-0">${tx.data && tx.data.length > 50 ? tx.data.substring(0, 49) + '...' : tx.data} ${copyToClipboard(() => { return tx.data })}</pre> |
||||
</div> |
||||
<div class="mb-3"> |
||||
<span class="text-dark mr-2">Amount:</span> |
||||
<span>${amount} Ether</span> |
||||
</div> |
||||
<div> |
||||
<span class="text-dark mr-2">Gas estimation:</span> |
||||
<span>${gasEstimation}</span> |
||||
</div> |
||||
<div> |
||||
<span class="text-dark mr-2">Gas limit:</span> |
||||
<span>${tx.gas}</span> |
||||
</div> |
||||
${ |
||||
network.lastBlock.baseFeePerGas ? yo` |
||||
<div class="align-items-center my-1" title="Represents the part of the tx fee that goes to the miner."> |
||||
<div class='d-flex'> |
||||
<span class="text-dark mr-2 text-nowrap">Max Priority fee:</span> |
||||
<input class="form-control mr-1 text-right" style='height: 1.2rem; width: 6rem;' value="1" id='maxpriorityfee' /> |
||||
<span title="visit https://ethgasstation.info for current gas price info.">Gwei</span> |
||||
</div> |
||||
</div> |
||||
<div class="align-items-center my-1" title="Represents the maximum amount of fee that you will pay for this transaction. The minimun needs to be set to base fee."> |
||||
<div class='d-flex'> |
||||
<span class="text-dark mr-2 text-nowrap">Max fee (Not less than base fee ${Web3.utils.fromWei(Web3.utils.toBN(parseInt(network.lastBlock.baseFeePerGas, 16)), 'Gwei')} Gwei):</span> |
||||
<input class="form-control mr-1 text-right" style='height: 1.2rem; width: 6rem;' id='maxfee' oninput=${onMaxFeeChange} /> |
||||
<span>Gwei</span> |
||||
<span class="text-dark ml-2"></span> |
||||
</div> |
||||
</div>` |
||||
: yo`<div class="d-flex align-items-center my-1">
|
||||
<span class="text-dark mr-2 text-nowrap">Gas price:</span> |
||||
<input class="form-control mr-1 text-right" style='width: 40px; height: 28px;' id='gasprice' oninput=${onGasPriceChange} /> |
||||
<span>Gwei (visit <a target='_blank' href='https://ethgasstation.info'>ethgasstation.info</a> for current gas price info.)</span> |
||||
</div>` |
||||
} |
||||
<div class="mb-3"> |
||||
<span class="text-dark mr-2">Max transaction fee:</span> |
||||
<span class="text-warning" id='txfee'></span> |
||||
</div> |
||||
</div> |
||||
<div class="d-flex py-1 align-items-center custom-control custom-checkbox ${css.checkbox}"> |
||||
<input class="form-check-input custom-control-input" id="confirmsetting" type="checkbox"> |
||||
<label class="m-0 form-check-label custom-control-label" for="confirmsetting">Do not show this warning again.</label> |
||||
</div> |
||||
</div> |
||||
` |
||||
|
||||
initialParamsCb((txFeeText, gasPriceValue, gasPriceStatus) => { |
||||
if (txFeeText) { |
||||
el.querySelector('#txfee').innerHTML = txFeeText |
||||
} |
||||
if (el.querySelector('#gasprice') && gasPriceValue) { |
||||
el.querySelector('#gasprice').value = gasPriceValue |
||||
onGasPriceChange() |
||||
} |
||||
if (el.querySelector('#maxfee') && network && network.lastBlock && network.lastBlock.baseFeePerGas) { |
||||
el.querySelector('#maxfee').value = Web3.utils.fromWei(Web3.utils.toBN(parseInt(network.lastBlock.baseFeePerGas, 16)), 'Gwei') |
||||
onMaxFeeChange() |
||||
} |
||||
if (gasPriceStatus !== undefined) { |
||||
el.gasPriceStatus = gasPriceStatus |
||||
} |
||||
}) |
||||
|
||||
return el |
||||
} |
||||
|
||||
module.exports = confirmDialog |
@ -1,83 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
// -------------- copyToClipboard ----------------------
|
||||
var csjs = require('csjs-inject') |
||||
|
||||
var css = csjs` |
||||
.container |
||||
{ |
||||
display: none; |
||||
position: fixed; |
||||
border-radius: 2px; |
||||
z-index: 1000; |
||||
box-shadow: 0 0 4px var(--dark); |
||||
} |
||||
.liitem |
||||
{ |
||||
padding: 2px; |
||||
padding-left: 6px; |
||||
cursor: pointer; |
||||
color: var(--text-dark); |
||||
background-color: var(--light); |
||||
} |
||||
.liitem:hover |
||||
{ |
||||
background-color: var(--secondary); |
||||
} |
||||
#menuitems |
||||
{ |
||||
list-style: none; |
||||
margin: 0px; |
||||
} |
||||
` |
||||
|
||||
module.exports = (event, items, linkItems) => { |
||||
event.preventDefault() |
||||
|
||||
function hide (event, force) { |
||||
if (container && container.parentElement && (force || (event.target !== container))) { |
||||
container.parentElement.removeChild(container) |
||||
} |
||||
window.removeEventListener('click', hide) |
||||
} |
||||
|
||||
const menu = Object.keys(items).map((item, index) => { |
||||
const current = yo`<li id="menuitem${item.toLowerCase()}" class=${css.liitem}>${item}</li>` |
||||
current.onclick = () => { hide(null, true); items[item]() } |
||||
return current |
||||
}) |
||||
|
||||
let menuForLinks = yo`` |
||||
if (linkItems) { |
||||
menuForLinks = Object.keys(linkItems).map((item, index) => { |
||||
const current = yo`<li id="menuitem${item.toLowerCase()}" class=${css.liitem}><a href=${linkItems[item]} target="_blank">${item}</a></li>` |
||||
current.onclick = () => { hide(null, true) } |
||||
return current |
||||
}) |
||||
} |
||||
|
||||
const container = yo` |
||||
<div id="menuItemsContainer" class="p-1 ${css.container} bg-light shadow border"> |
||||
<ul id='menuitems'>${menu} ${menuForLinks}</ul> |
||||
</div> |
||||
` |
||||
|
||||
container.style.left = event.pageX + 'px' |
||||
container.style.top = event.pageY + 'px' |
||||
container.style.display = 'block' |
||||
document.querySelector('body').appendChild(container) |
||||
|
||||
const menuItemsContainer = document.getElementById('menuItemsContainer') |
||||
const boundary = menuItemsContainer.getBoundingClientRect() |
||||
|
||||
if (boundary.bottom > (window.innerHeight || document.documentElement.clientHeight)) { |
||||
menuItemsContainer.style.position = 'absolute' |
||||
menuItemsContainer.style.bottom = '10px' |
||||
menuItemsContainer.style.top = null |
||||
} |
||||
|
||||
setTimeout(() => { |
||||
window.addEventListener('click', hide) |
||||
}, 500) |
||||
|
||||
return { hide } |
||||
} |
@ -1,39 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
// -------------- copyToClipboard ----------------------
|
||||
const copy = require('copy-to-clipboard') |
||||
var addTooltip = require('./tooltip') |
||||
// -------------- styling ----------------------
|
||||
var csjs = require('csjs-inject') |
||||
|
||||
var css = csjs` |
||||
.copyIcon { |
||||
margin-left: 5px; |
||||
cursor: pointer; |
||||
} |
||||
` |
||||
|
||||
module.exports = function copyToClipboard (getContent, tip = 'Copy value to clipboard', icon = 'fa-copy') { |
||||
var copyIcon = yo`<i title="${tip}" class="${css.copyIcon} far ${icon} p-2" data-id="copyToClipboardCopyIcon" aria-hidden="true"></i>` |
||||
copyIcon.onclick = (event) => { |
||||
event.stopPropagation() |
||||
var copiableContent |
||||
try { |
||||
copiableContent = getContent() |
||||
} catch (e) { |
||||
addTooltip(e.message) |
||||
return |
||||
} |
||||
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('Copied value to clipboard.') |
||||
} else { |
||||
addTooltip('Cannot copy empty content!') |
||||
} |
||||
} |
||||
return copyIcon |
||||
} |
@ -1,131 +0,0 @@ |
||||
'use strict' |
||||
var yo = require('yo-yo') |
||||
var csjs = require('csjs-inject') |
||||
|
||||
var css = csjs` |
||||
.containerDraggableModal { |
||||
position: absolute; |
||||
z-index: 1000; |
||||
text-align: center; |
||||
width: 500px; |
||||
height: 500px; |
||||
overflow-y: hidden; |
||||
} |
||||
|
||||
.headerDraggableModal { |
||||
cursor: move; |
||||
z-index: 10; |
||||
text-overflow: ellipsis; |
||||
overflow-x: hidden; |
||||
} |
||||
|
||||
.modalActions { |
||||
float: right; |
||||
} |
||||
|
||||
.modalAction { |
||||
padding-right: 1px; |
||||
padding-left: 1px; |
||||
cursor: pointer; |
||||
} |
||||
` |
||||
|
||||
module.exports = |
||||
class DraggableContent { |
||||
constructor (closeCb) { |
||||
this.closeCb = closeCb |
||||
this.isMaximised = false |
||||
} |
||||
|
||||
render (title, url, content) { |
||||
this.content = content |
||||
var el = yo` |
||||
<div class=${css.containerDraggableModal}> |
||||
<div> |
||||
<div class="${css.headerDraggableModal} title" title=${title}><span title="${title}" >${title}</span><span title="${url}" > - ${url}</span> |
||||
<div class=${css.modalActions}> |
||||
<i onclick=${() => { this.minimize() }} class="fas fa-window-minimize ${css.modalAction}"></i> |
||||
<i onclick=${() => { this.maximise() }} class="fas fa-window-maximize ${css.modalAction}"></i> |
||||
<i onclick=${() => { this.close() }} class="fas fa-window-close-o ${css.modalAction}"></i> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
${content} |
||||
</div> ` |
||||
dragElement(el) |
||||
el.style.top = '20%' |
||||
el.style.left = '50%' |
||||
this.el = el |
||||
return el |
||||
} |
||||
|
||||
setTitle (title) { |
||||
this.el.querySelector('.title span').innerHTML = title |
||||
} |
||||
|
||||
minimize () { |
||||
this.isMaximised = false |
||||
this.content.style.display = 'none' |
||||
this.el.style.height = 'inherit' |
||||
this.el.style.width = '150px' |
||||
this.el.querySelector('.title').style.width = '146px' |
||||
} |
||||
|
||||
maximise () { |
||||
this.content.style.display = 'block' |
||||
var body = document.querySelector('body') |
||||
this.el.style.height = this.isMaximised ? '500px' : body.offsetHeight + 'px' |
||||
this.el.style.width = this.isMaximised ? '500px' : body.offsetWidth + 'px' |
||||
this.isMaximised = !this.isMaximised |
||||
this.el.style.top = this.isMaximised ? '0%' : '20%' |
||||
this.el.style.left = this.isMaximised ? '0%' : '50%' |
||||
this.el.querySelector('.title').style.width = 'inherit' |
||||
} |
||||
|
||||
close () { |
||||
if (this.closeCb) this.closeCb() |
||||
if (this.el.parentElement) { |
||||
this.el.parentElement.removeChild(this.el) |
||||
} |
||||
} |
||||
} |
||||
|
||||
function dragElement (elmnt) { |
||||
var pos1 = 0 |
||||
var pos2 = 0 |
||||
var pos3 = 0 |
||||
var pos4 = 0 |
||||
|
||||
elmnt.querySelector('.title').onmousedown = dragMouseDown |
||||
|
||||
function dragMouseDown (e) { |
||||
e = e || window.event |
||||
if (e.button !== 0) return |
||||
e.preventDefault() |
||||
// get the mouse cursor position at startup:
|
||||
pos3 = e.clientX |
||||
pos4 = e.clientY |
||||
document.onmouseup = closeDragElement |
||||
// call a function whenever the cursor moves:
|
||||
document.onmousemove = elementDrag |
||||
} |
||||
|
||||
function elementDrag (e) { |
||||
e = e || window.event |
||||
e.preventDefault() |
||||
// calculate the new cursor position:
|
||||
pos1 = pos3 - e.clientX |
||||
pos2 = pos4 - e.clientY |
||||
pos3 = e.clientX |
||||
pos4 = e.clientY |
||||
// set the element's new position:
|
||||
elmnt.style.top = (elmnt.offsetTop - pos2) + 'px' |
||||
elmnt.style.left = (elmnt.offsetLeft - pos1) + 'px' |
||||
} |
||||
|
||||
function closeDragElement () { |
||||
/* stop moving when mouse button is released: */ |
||||
document.onmouseup = null |
||||
document.onmousemove = null |
||||
} |
||||
} |
@ -1,128 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var EventManager = require('../../lib/events') |
||||
|
||||
// -------------- styling ----------------------
|
||||
|
||||
var css = require('./styles/dropdown-styles') |
||||
|
||||
/* USAGE: |
||||
|
||||
var dropdown = new Dropdown({ |
||||
options: [ |
||||
'knownTransaction', |
||||
'unknownTransaction', |
||||
'log', |
||||
'info', |
||||
'error' |
||||
], |
||||
defaults: ['knownTransaction'] |
||||
}) |
||||
dropdown.event.register('deselect', function (label) { }) |
||||
dropdown.event.register('select', function (label) { }) |
||||
|
||||
*/ |
||||
class Dropdown { |
||||
constructor (opts = {}) { |
||||
var self = this |
||||
self.event = new EventManager() |
||||
self.data = { |
||||
_options: opts.options || [], |
||||
_dependencies: opts.dependencies || [], |
||||
selected: opts.defaults || [], |
||||
_elements: [] |
||||
} |
||||
self._view = {} |
||||
self._api = opts.api |
||||
self._events = opts.events |
||||
} |
||||
|
||||
render () { |
||||
var self = this |
||||
if (self._view.el) return self._view.el |
||||
self._view.selected = yo` |
||||
<div class=${css.selectbox}> |
||||
<span class=${css.selected}> [${self.data.selected.length}] ${self.data.selected.join(', ')}</span> |
||||
<i class="${css.icon} fas fa-caret-down"></i> |
||||
</div> |
||||
` |
||||
self._view.el = yo` |
||||
<div name="dropdown" class="${css.dropdown} form-control form-control-sm" onclick=${show}> |
||||
${self._view.selected} |
||||
<div class="${css.options} bg-light" style="display: none;}"> |
||||
${self.data._options.map(label => { |
||||
const index = self.data._elements.length |
||||
var input = yo` |
||||
<input |
||||
data-idx=${index} |
||||
onchange=${emit} |
||||
type="${index === 2 ? 'checkbox' : 'radio'}" |
||||
id="${label}" |
||||
/> |
||||
` |
||||
if (self.data.selected.indexOf(label) !== -1) { |
||||
input.checked = true |
||||
self.event.trigger('select', [label]) |
||||
} |
||||
self.data._elements.push(input) |
||||
return yo` |
||||
<div class=${css.option}> |
||||
${input} |
||||
<label class="text-dark" for="${label}">${label}</label> |
||||
</div> |
||||
` |
||||
})} |
||||
</div> |
||||
</div> |
||||
` |
||||
return self._view.el |
||||
function emit (event) { |
||||
var input = event.currentTarget |
||||
var label = input.nextSibling.innerText |
||||
if (input.checked) { |
||||
self.data.selected.push(label) |
||||
if (event.type === 'change') { |
||||
self.event.trigger('select', [label]) |
||||
updateDependencies(label) |
||||
} |
||||
} else { |
||||
var idx = self.data.selected.indexOf(label) |
||||
self.data.selected.splice(idx, 1) |
||||
if (event.type === 'change') { |
||||
self.event.trigger('deselect', [label]) |
||||
updateDependencies(label) |
||||
} |
||||
} |
||||
self._view.selected.children[0].innerText = `[${self.data.selected.length}] ${self.data.selected.join(', ')}` |
||||
} |
||||
function updateDependencies (changed) { |
||||
if (self.data._dependencies[changed]) { |
||||
for (var dep in self.data._dependencies[changed]) { |
||||
var label = self.data._dependencies[changed][dep] |
||||
var el = self.data._elements[self.data._options.indexOf(label)] |
||||
el.checked = !el.checked |
||||
emit({ currentTarget: el, type: 'changeDependencies' }) |
||||
} |
||||
} |
||||
} |
||||
function show (event) { |
||||
event.stopPropagation() |
||||
var options = event.currentTarget.children[1] |
||||
var parent = event.target.parentElement |
||||
var isOption = parent === options || parent.parentElement === options |
||||
if (isOption) return |
||||
if (options.style.display === 'none') { |
||||
options.style.display = '' |
||||
document.body.addEventListener('click', handler) |
||||
} else { |
||||
options.style.display = 'none' |
||||
document.body.removeEventListener('click', handler) |
||||
} |
||||
function handler (event) { |
||||
options.style.display = 'none' |
||||
document.body.removeEventListener('click', handler) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
module.exports = Dropdown |
@ -1,228 +0,0 @@ |
||||
'use strict' |
||||
|
||||
var yo = require('yo-yo') |
||||
var css = require('../../universal-dapp-styles') |
||||
var copyToClipboard = require('./copy-to-clipboard') |
||||
var remixLib = require('@remix-project/remix-lib') |
||||
var txFormat = remixLib.execution.txFormat |
||||
|
||||
class MultiParamManager { |
||||
/** |
||||
* |
||||
* @param {bool} lookupOnly |
||||
* @param {Object} funABI |
||||
* @param {Function} clickMultiCallBack |
||||
* @param {string} inputs |
||||
* @param {string} title |
||||
* @param {string} evmBC |
||||
* |
||||
*/ |
||||
constructor (lookupOnly, funABI, clickCallBack, inputs, title, evmBC, isDeploy) { |
||||
this.lookupOnly = lookupOnly |
||||
this.funABI = funABI |
||||
this.clickCallBack = clickCallBack |
||||
this.inputs = inputs |
||||
this.title = title |
||||
this.evmBC = evmBC |
||||
this.basicInputField = null |
||||
this.multiFields = null |
||||
this.isDeploy = isDeploy |
||||
} |
||||
|
||||
switchMethodViewOn () { |
||||
this.contractActionsContainerSingle.style.display = 'none' |
||||
this.contractActionsContainerMulti.style.display = 'flex' |
||||
this.makeMultiVal() |
||||
} |
||||
|
||||
switchMethodViewOff () { |
||||
this.contractActionsContainerSingle.style.display = 'flex' |
||||
this.contractActionsContainerMulti.style.display = 'none' |
||||
var multiValString = this.getMultiValsString() |
||||
if (multiValString) this.basicInputField.value = multiValString |
||||
} |
||||
|
||||
getValue (item, index) { |
||||
var valStr = item.value.join('') |
||||
return valStr |
||||
} |
||||
|
||||
getMultiValsString () { |
||||
var valArray = this.multiFields.querySelectorAll('input') |
||||
var ret = '' |
||||
var valArrayTest = [] |
||||
|
||||
for (var j = 0; j < valArray.length; j++) { |
||||
if (ret !== '') ret += ',' |
||||
var elVal = valArray[j].value |
||||
valArrayTest.push(elVal) |
||||
elVal = elVal.replace(/(^|,\s+|,)(\d+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted number by quoted number
|
||||
elVal = elVal.replace(/(^|,\s+|,)(0[xX][0-9a-fA-F]+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted hex string by quoted hex string
|
||||
try { |
||||
JSON.parse(elVal) |
||||
} catch (e) { |
||||
elVal = '"' + elVal + '"' |
||||
} |
||||
ret += elVal |
||||
} |
||||
var valStringTest = valArrayTest.join('') |
||||
if (valStringTest) { |
||||
return ret |
||||
} else { |
||||
return '' |
||||
} |
||||
} |
||||
|
||||
emptyInputs () { |
||||
var valArray = this.multiFields.querySelectorAll('input') |
||||
for (var k = 0; k < valArray.length; k++) { |
||||
valArray[k].value = '' |
||||
} |
||||
this.basicInputField.value = '' |
||||
} |
||||
|
||||
makeMultiVal () { |
||||
var inputString = this.basicInputField.value |
||||
if (inputString) { |
||||
inputString = inputString.replace(/(^|,\s+|,)(\d+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted number by quoted number
|
||||
inputString = inputString.replace(/(^|,\s+|,)(0[xX][0-9a-fA-F]+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted hex string by quoted hex string
|
||||
var inputJSON = JSON.parse('[' + inputString + ']') |
||||
var multiInputs = this.multiFields.querySelectorAll('input') |
||||
for (var k = 0; k < multiInputs.length; k++) { |
||||
if (inputJSON[k]) { |
||||
multiInputs[k].value = JSON.stringify(inputJSON[k]) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
createMultiFields () { |
||||
if (this.funABI.inputs) { |
||||
return yo`<div>
|
||||
${this.funABI.inputs.map(function (inp) { |
||||
return yo`<div class="${css.multiArg}"><label for="${inp.name}"> ${inp.name}: </label><input class="form-control" placeholder="${inp.type}" title="${inp.name}" data-id="multiParamManagerInput${inp.name}"></div>` |
||||
})} |
||||
</div>` |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
var title |
||||
if (this.title) { |
||||
title = this.title |
||||
} else if (this.funABI.name) { |
||||
title = this.funABI.name |
||||
} else { |
||||
title = this.funABI.type === 'receive' ? '(receive)' : '(fallback)' |
||||
} |
||||
|
||||
this.basicInputField = yo`<input class="form-control" data-id="multiParamManagerBasicInputField"></input>` |
||||
this.basicInputField.setAttribute('placeholder', this.inputs) |
||||
this.basicInputField.setAttribute('title', this.inputs) |
||||
this.basicInputField.setAttribute('data-id', this.inputs) |
||||
|
||||
var onClick = () => { |
||||
this.clickCallBack(this.funABI.inputs, this.basicInputField.value) |
||||
} |
||||
const width = this.isDeploy ? '' : 'w-50' |
||||
const funcButton = yo`<button onclick=${() => onClick()} class="${css.instanceButton} ${width} btn btn-sm" data-id="multiParamManagerFuncButton">${title}</button>` |
||||
this.contractActionsContainerSingle = yo` |
||||
<div class="${css.contractActionsContainerSingle} pt-2"> |
||||
${funcButton} |
||||
${this.basicInputField} |
||||
<i class="fas fa-angle-down ${css.methCaret}" onclick=${() => this.switchMethodViewOn()} title=${title} ></i> |
||||
</div>` |
||||
|
||||
this.multiFields = this.createMultiFields() |
||||
|
||||
var multiOnClick = () => { |
||||
var valsString = this.getMultiValsString() |
||||
if (valsString) { |
||||
this.clickCallBack(this.funABI.inputs, valsString) |
||||
} else { |
||||
this.clickCallBack(this.funABI.inputs, '') |
||||
} |
||||
} |
||||
|
||||
var expandedButton = yo`<button onclick=${() => { multiOnClick() }} class="${css.instanceButton}" data-id="multiParamManagerExpandedButton"></button>` |
||||
|
||||
this.contractActionsContainerMulti = yo`<div class="${css.contractActionsContainerMulti}" >
|
||||
<div class="${css.contractActionsContainerMultiInner} text-dark" > |
||||
<div onclick=${() => { this.switchMethodViewOff() }} class="${css.multiHeader}"> |
||||
<div class="${css.multiTitle} run-instance-multi-title">${title}</div> |
||||
<i class='fas fa-angle-up ${css.methCaret}'></i> |
||||
</div> |
||||
${this.multiFields} |
||||
<div class="${css.group} ${css.multiArg}" > |
||||
${copyToClipboard( |
||||
() => { |
||||
var multiString = this.getMultiValsString() |
||||
var multiJSON = JSON.parse('[' + multiString + ']') |
||||
var encodeObj |
||||
if (this.evmBC) { |
||||
encodeObj = txFormat.encodeData(this.funABI, multiJSON, this.evmBC) |
||||
} else { |
||||
encodeObj = txFormat.encodeData(this.funABI, multiJSON) |
||||
} |
||||
if (encodeObj.error) { |
||||
throw new Error(encodeObj.error) |
||||
} else { |
||||
return encodeObj.data |
||||
} |
||||
}, 'Encode values of input fields & copy to clipboard', 'fa-clipboard')} |
||||
${expandedButton} |
||||
</div> |
||||
</div> |
||||
</div>` |
||||
|
||||
var contractProperty = yo` |
||||
<div class="${css.contractProperty}"> |
||||
${this.contractActionsContainerSingle} ${this.contractActionsContainerMulti} |
||||
</div> |
||||
` |
||||
if (this.lookupOnly) { |
||||
// call. stateMutability is either pure or view
|
||||
expandedButton.setAttribute('title', (title + ' - call')) |
||||
expandedButton.innerHTML = 'call' |
||||
expandedButton.classList.add('btn-info') |
||||
expandedButton.setAttribute('data-id', (title + ' - call')) |
||||
funcButton.setAttribute('title', (title + ' - call')) |
||||
funcButton.classList.add('btn-info') |
||||
funcButton.setAttribute('data-id', (title + ' - call')) |
||||
} else if (this.funABI.stateMutability === 'payable' || this.funABI.payable) { |
||||
// transact. stateMutability = payable
|
||||
expandedButton.setAttribute('title', (title + ' - transact (payable)')) |
||||
expandedButton.innerHTML = 'transact' |
||||
expandedButton.classList.add('btn-danger') |
||||
expandedButton.setAttribute('data-id', (title + ' - transact (payable)')) |
||||
funcButton.setAttribute('title', (title + ' - transact (payable)')) |
||||
funcButton.classList.add('btn-danger') |
||||
funcButton.setAttribute('data-id', (title + ' - transact (payable)')) |
||||
} else { |
||||
// transact. stateMutability = nonpayable
|
||||
expandedButton.setAttribute('title', (title + ' - transact (not payable)')) |
||||
expandedButton.innerHTML = 'transact' |
||||
expandedButton.classList.add('btn-warning') |
||||
expandedButton.setAttribute('data-id', (title + ' - transact (not payable)')) |
||||
funcButton.classList.add('btn-warning') |
||||
funcButton.setAttribute('title', (title + ' - transact (not payable)')) |
||||
funcButton.setAttribute('data-id', (title + ' - transact (not payable)')) |
||||
} |
||||
|
||||
if (this.funABI.inputs && this.funABI.inputs.length > 0) { |
||||
contractProperty.classList.add(css.hasArgs) |
||||
} else if (this.funABI.type === 'fallback' || this.funABI.type === 'receive') { |
||||
contractProperty.classList.add(css.hasArgs) |
||||
this.basicInputField.setAttribute('title', `'(${this.funABI.type}')`) // probably should pass name instead
|
||||
this.contractActionsContainerSingle.querySelector('i').style.visibility = 'hidden' |
||||
this.basicInputField.setAttribute('data-id', `'(${this.funABI.type}')`) |
||||
} else { |
||||
this.contractActionsContainerSingle.querySelector('i').style.visibility = 'hidden' |
||||
this.basicInputField.style.visibility = 'hidden' |
||||
} |
||||
|
||||
return contractProperty |
||||
} |
||||
} |
||||
|
||||
module.exports = MultiParamManager |
@ -1,153 +0,0 @@ |
||||
'use strict' |
||||
|
||||
var $ = require('jquery') |
||||
var yo = require('yo-yo') |
||||
const { default: Registry } = require('../state/registry') |
||||
var css = require('./styles/renderer-styles') |
||||
|
||||
/** |
||||
* After refactor, the renderer is only used to render error/warning |
||||
* TODO: This don't need to be an object anymore. Simplify and just export the renderError function. |
||||
* |
||||
*/ |
||||
function Renderer (service) { |
||||
const self = this |
||||
self.service = service |
||||
self._components = {} |
||||
self._components.registry = Registry.getInstance() |
||||
// dependencies
|
||||
self._deps = { |
||||
fileManager: self._components.registry.get('filemanager').api, |
||||
config: self._components.registry.get('config').api |
||||
} |
||||
if (document && document.head) { |
||||
document.head.appendChild(css) |
||||
} |
||||
} |
||||
|
||||
Renderer.prototype._error = function (file, error) { |
||||
const self = this |
||||
if (file === self._deps.config.get('currentFile')) { |
||||
self.service.call('editor', 'addAnnotation', error, file) |
||||
} |
||||
} |
||||
|
||||
Renderer.prototype._errorClick = function (errFile, errLine, errCol) { |
||||
const self = this |
||||
const editor = self._components.registry.get('editor').api |
||||
if (errFile !== self._deps.config.get('currentFile')) { |
||||
// TODO: refactor with this._components.contextView.jumpTo
|
||||
var provider = self._deps.fileManager.fileProviderOf(errFile) |
||||
if (provider) { |
||||
provider.exists(errFile).then(exist => { |
||||
self._deps.fileManager.open(errFile) |
||||
editor.gotoLine(errLine, errCol) |
||||
}).catch(error => { |
||||
if (error) return console.log(error) |
||||
}) |
||||
} |
||||
} else { |
||||
editor.gotoLine(errLine, errCol) |
||||
} |
||||
} |
||||
|
||||
function getPositionDetails (msg) { |
||||
const result = {} |
||||
|
||||
// To handle some compiler warning without location like SPDX license warning etc
|
||||
if (!msg.includes(':')) return { errLine: -1, errCol: -1, errFile: msg } |
||||
|
||||
// extract line / column
|
||||
let position = msg.match(/^(.*?):([0-9]*?):([0-9]*?)?/) |
||||
result.errLine = position ? parseInt(position[2]) - 1 : -1 |
||||
result.errCol = position ? parseInt(position[3]) : -1 |
||||
|
||||
// extract file
|
||||
position = msg.match(/^(https:.*?|http:.*?|.*?):/) |
||||
result.errFile = position ? position[1] : '' |
||||
return result |
||||
} |
||||
|
||||
/** |
||||
* format msg like error or warning, |
||||
* |
||||
* @param {String or DOMElement} message |
||||
* @param {DOMElement} container |
||||
* @param {Object} options { |
||||
* useSpan, |
||||
* noAnnotations, |
||||
* click:(Function), |
||||
* type:( |
||||
* warning, |
||||
* error |
||||
* ), |
||||
* errFile, |
||||
* errLine, |
||||
* errCol |
||||
* } |
||||
*/ |
||||
|
||||
Renderer.prototype.error = function (message, container, opt) { |
||||
if (!message) return |
||||
if (container === undefined) return |
||||
opt = opt || {} |
||||
|
||||
var text |
||||
if (typeof message === 'string') { |
||||
text = message |
||||
message = yo`<span>${message}</span>` |
||||
} else if (message.innerText) { |
||||
text = message.innerText |
||||
} |
||||
|
||||
// ^ e.g:
|
||||
// browser/gm.sol: Warning: Source file does not specify required compiler version! Consider adding "pragma solidity ^0.6.12
|
||||
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/introspection/IERC1820Registry.sol:3:1: ParserError: Source file requires different compiler version (current compiler is 0.7.4+commit.3f05b770.Emscripten.clang) - note that nightly builds are considered to be strictly less than the released version
|
||||
|
||||
let position = getPositionDetails(text) |
||||
|
||||
// For compiler version 0.8.0 and upcoming versions, errors and warning will be reported in a different way
|
||||
// Above method regex will return type of error as 'errFile'
|
||||
// Comparison of 'errFile' with passed error type will ensure the reporter type
|
||||
if (!position.errFile || (opt.errorType && opt.errorType === position.errFile)) { |
||||
// Updated error reported includes '-->' before file details
|
||||
const errorDetails = text.split('-->') |
||||
// errorDetails[1] will have file details
|
||||
if (errorDetails.length > 1) position = getPositionDetails(errorDetails[1]) |
||||
} |
||||
|
||||
opt.errLine = position.errLine |
||||
opt.errCol = position.errCol |
||||
opt.errFile = position.errFile.trim() |
||||
|
||||
if (!opt.noAnnotations && opt.errFile) { |
||||
this._error(opt.errFile, { |
||||
row: opt.errLine, |
||||
column: opt.errCol, |
||||
text: text, |
||||
type: opt.type |
||||
}) |
||||
} |
||||
|
||||
var $pre = $(opt.useSpan ? yo`<span></span>` : yo`<pre></pre>`).html(message) |
||||
|
||||
const classList = opt.type === 'error' ? 'alert alert-danger' : 'alert alert-warning' |
||||
var $error = $(yo`<div class="sol ${opt.type} ${classList}" data-id="${opt.errFile}"><div class="close" data-id="renderer"><i class="fas fa-times"></i></div></div>`).prepend($pre) |
||||
$(container).append($error) |
||||
|
||||
$error.click((ev) => { |
||||
if (opt.click) { |
||||
opt.click(message) |
||||
} else if (opt.errFile !== undefined && opt.errLine !== undefined && opt.errCol !== undefined) { |
||||
this._errorClick(opt.errFile, opt.errLine, opt.errCol) |
||||
} |
||||
}) |
||||
|
||||
$error.find('.close').click(function (ev) { |
||||
ev.preventDefault() |
||||
$error.remove() |
||||
return false |
||||
}) |
||||
} |
||||
|
||||
module.exports = Renderer |
@ -1,109 +0,0 @@ |
||||
const yo = require('yo-yo') |
||||
const remixLib = require('@remix-project/remix-lib') |
||||
const confirmDialog = require('./confirmDialog') |
||||
const modalCustom = require('./modal-dialog-custom') |
||||
const modalDialog = require('./modaldialog') |
||||
const typeConversion = remixLib.execution.typeConversion |
||||
const Web3 = require('web3') |
||||
|
||||
module.exports = { |
||||
getCallBacksWithContext: (udappUI, blockchain) => { |
||||
const callbacks = {} |
||||
callbacks.confirmationCb = confirmationCb |
||||
callbacks.continueCb = continueCb |
||||
callbacks.promptCb = promptCb |
||||
callbacks.udappUI = udappUI |
||||
callbacks.blockchain = blockchain |
||||
return callbacks |
||||
} |
||||
} |
||||
|
||||
const continueCb = function (error, continueTxExecution, cancelCb) { |
||||
if (error) { |
||||
const msg = typeof error !== 'string' ? error.message : error |
||||
modalDialog( |
||||
'Gas estimation failed', |
||||
yo` |
||||
<div>Gas estimation errored with the following message (see below). |
||||
The transaction execution will likely fail. Do you want to force sending? <br>${msg}</div> |
||||
`,
|
||||
{ |
||||
label: 'Send Transaction', |
||||
fn: () => continueTxExecution() |
||||
}, |
||||
{ |
||||
label: 'Cancel Transaction', |
||||
fn: () => cancelCb() |
||||
} |
||||
) |
||||
} else { |
||||
continueTxExecution() |
||||
} |
||||
} |
||||
|
||||
const promptCb = function (okCb, cancelCb) { |
||||
modalCustom.promptPassphrase('Passphrase requested', 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) |
||||
} |
||||
|
||||
const confirmationCb = function (network, tx, gasEstimation, continueTxExecution, cancelCb) { |
||||
const self = this |
||||
if (network.name !== 'Main') { |
||||
return continueTxExecution(null) |
||||
} |
||||
var amount = Web3.utils.fromWei(typeConversion.toInt(tx.value), 'ether') |
||||
var content = confirmDialog(tx, network, amount, gasEstimation, |
||||
(gasPrice, cb) => { |
||||
let txFeeText, priceStatus |
||||
// TODO: this try catch feels like an anti pattern, can/should be
|
||||
// removed, but for now keeping the original logic
|
||||
try { |
||||
var fee = Web3.utils.toBN(tx.gas).mul(Web3.utils.toBN(Web3.utils.toWei(gasPrice.toString(10), 'gwei'))) |
||||
txFeeText = ' ' + Web3.utils.fromWei(fee.toString(10), 'ether') + ' Ether' |
||||
priceStatus = true |
||||
} catch (e) { |
||||
txFeeText = ' Please fix this issue before sending any transaction. ' + e.message |
||||
priceStatus = false |
||||
} |
||||
cb(txFeeText, priceStatus) |
||||
}, |
||||
(cb) => { |
||||
self.blockchain.web3().eth.getGasPrice((error, gasPrice) => { |
||||
const warnMessage = ' Please fix this issue before sending any transaction. ' |
||||
if (error) { |
||||
return cb('Unable to retrieve the current network gas price.' + warnMessage + error) |
||||
} |
||||
try { |
||||
var gasPriceValue = Web3.utils.fromWei(gasPrice.toString(10), 'gwei') |
||||
cb(null, gasPriceValue) |
||||
} catch (e) { |
||||
cb(warnMessage + e.message, null, false) |
||||
} |
||||
}) |
||||
} |
||||
) |
||||
modalDialog( |
||||
'Confirm transaction', |
||||
content, |
||||
{ |
||||
label: 'Confirm', |
||||
fn: () => { |
||||
self.blockchain.config.setUnpersistedProperty( |
||||
'doNotShowTransactionConfirmationAgain', |
||||
content.querySelector('input#confirmsetting').checked |
||||
) |
||||
// TODO: check if this is check is still valid given the refactor
|
||||
if (!content.gasPriceStatus) { |
||||
cancelCb('Given transaction fee is not correct') |
||||
} else { |
||||
continueTxExecution(content.txFee) |
||||
} |
||||
} |
||||
}, |
||||
{ |
||||
label: 'Cancel', |
||||
fn: () => { |
||||
return cancelCb('Transaction canceled by user.') |
||||
} |
||||
} |
||||
) |
||||
} |
@ -1,52 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
|
||||
var css = yo`<style>
|
||||
.sol.success, |
||||
.sol.error, |
||||
.sol.warning { |
||||
white-space: pre-line; |
||||
word-wrap: break-word; |
||||
cursor: pointer; |
||||
position: relative; |
||||
margin: 0.5em 0 1em 0; |
||||
border-radius: 5px; |
||||
line-height: 20px; |
||||
padding: 8px 15px; |
||||
} |
||||
|
||||
.sol.success pre, |
||||
.sol.error pre, |
||||
.sol.warning pre { |
||||
white-space: pre-line; |
||||
overflow-y: hidden; |
||||
background-color: transparent; |
||||
margin: 0; |
||||
font-size: 12px; |
||||
border: 0 none; |
||||
padding: 0; |
||||
border-radius: 0; |
||||
} |
||||
|
||||
.sol.success .close, |
||||
.sol.error .close, |
||||
.sol.warning .close { |
||||
white-space: pre-line; |
||||
font-weight: bold; |
||||
position: absolute; |
||||
color: hsl(0, 0%, 0%); /* black in style-guide.js */ |
||||
top: 0; |
||||
right: 0; |
||||
padding: 0.5em; |
||||
} |
||||
|
||||
.sol.error { |
||||
} |
||||
|
||||
.sol.warning { |
||||
} |
||||
|
||||
.sol.success { |
||||
/* background-color: // styles.rightPanel.message_Success_BackgroundColor; */ |
||||
}</style>` |
||||
|
||||
module.exports = css |
@ -1,8 +0,0 @@ |
||||
import yo from 'yo-yo' |
||||
export function basicLogo () { |
||||
return yo`<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 105 100">
|
||||
<path d="M91.84,35a.09.09,0,0,1-.1-.07,41,41,0,0,0-79.48,0,.09.09,0,0,1-.1.07C9.45,35,1,35.35,1,42.53c0,8.56,1,16,6,20.32,2.16,1.85,5.81,2.3,9.27,2.22a44.4,44.4,0,0,0,6.45-.68.09.09,0,0,0,.06-.15A34.81,34.81,0,0,1,17,45c0-.1,0-.21,0-.31a35,35,0,0,1,70,0c0,.1,0,.21,0,.31a34.81,34.81,0,0,1-5.78,19.24.09.09,0,0,0,.06.15,44.4,44.4,0,0,0,6.45.68c3.46.08,7.11-.37,9.27-2.22,5-4.27,6-11.76,6-20.32C103,35.35,94.55,35,91.84,35Z"/> |
||||
<path d="M52,74,25.4,65.13a.1.1,0,0,0-.1.17L51.93,91.93a.1.1,0,0,0,.14,0L78.7,65.3a.1.1,0,0,0-.1-.17L52,74A.06.06,0,0,1,52,74Z"/> |
||||
<path d="M75.68,46.9,82,45a.09.09,0,0,0,.08-.09,29.91,29.91,0,0,0-.87-6.94.11.11,0,0,0-.09-.08l-6.43-.58a.1.1,0,0,1-.06-.18l4.78-4.18a.13.13,0,0,0,0-.12,30.19,30.19,0,0,0-3.65-6.07.09.09,0,0,0-.11,0l-5.91,2a.1.1,0,0,1-.12-.14L72.19,23a.11.11,0,0,0,0-.12,29.86,29.86,0,0,0-5.84-4.13.09.09,0,0,0-.11,0l-4.47,4.13a.1.1,0,0,1-.17-.07l.09-6a.1.1,0,0,0-.07-.1,30.54,30.54,0,0,0-7-1.47.1.1,0,0,0-.1.07l-2.38,5.54a.1.1,0,0,1-.18,0l-2.37-5.54a.11.11,0,0,0-.11-.06,30,30,0,0,0-7,1.48.12.12,0,0,0-.07.1l.08,6.05a.09.09,0,0,1-.16.07L37.8,18.76a.11.11,0,0,0-.12,0,29.75,29.75,0,0,0-5.83,4.13.11.11,0,0,0,0,.12l2.59,5.6a.11.11,0,0,1-.13.14l-5.9-2a.11.11,0,0,0-.12,0,30.23,30.23,0,0,0-3.62,6.08.11.11,0,0,0,0,.12l4.79,4.19a.1.1,0,0,1-.06.17L23,37.91a.1.1,0,0,0-.09.07A29.9,29.9,0,0,0,22,44.92a.1.1,0,0,0,.07.1L28.4,47a.1.1,0,0,1,0,.18l-5.84,3.26a.16.16,0,0,0,0,.11,30.17,30.17,0,0,0,2.1,6.76c.32.71.67,1.4,1,2.08a.1.1,0,0,0,.06,0L52,68.16H52l26.34-8.78a.1.1,0,0,0,.06-.05,30.48,30.48,0,0,0,3.11-8.88.1.1,0,0,0-.05-.11l-5.83-3.26A.1.1,0,0,1,75.68,46.9Z"/> |
||||
</svg>` |
||||
} |
@ -1,590 +0,0 @@ |
||||
'use strict' |
||||
var yo = require('yo-yo') |
||||
var copyToClipboard = require('./copy-to-clipboard') |
||||
|
||||
// -------------- styling ----------------------
|
||||
var csjs = require('csjs-inject') |
||||
var remixLib = require('@remix-project/remix-lib') |
||||
|
||||
var EventManager = require('../../lib/events') |
||||
var helper = require('../../lib/helper') |
||||
var modalDialog = require('./modal-dialog-custom') |
||||
const { default: Registry } = require('../state/registry') |
||||
|
||||
var typeConversion = remixLib.execution.typeConversion |
||||
|
||||
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 = Registry.getInstance().get('eventsDecoder').api |
||||
this.txListener = Registry.getInstance().get('txlistener').api |
||||
this.terminal = terminal |
||||
// dependencies
|
||||
this._deps = { |
||||
compilersArtefacts: Registry.getInstance().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 isDescendant (parent, child) { |
||||
var node = child.parentNode |
||||
while (node != null) { |
||||
if (node === parent) { |
||||
return true |
||||
} |
||||
node = node.parentNode |
||||
} |
||||
return false |
||||
} |
||||
|
||||
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 |
||||
} |
||||
|
||||
const tables = blockElement.querySelectorAll(`#${tx.id} [class^="txTable"]`) |
||||
const logs = blockElement.querySelectorAll(`#${tx.id} [class^='log']`) |
||||
const arrows = blockElement.querySelectorAll(`#${tx.id} [class^='arrow']`) |
||||
|
||||
let table = [...tables].filter((t) => isDescendant(tx, t))[0] |
||||
const log = [...logs].filter((t) => isDescendant(tx, t))[0] |
||||
const arrow = [...arrows].filter((t) => isDescendant(tx, t))[0] |
||||
|
||||
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 |
||||
} |
@ -1,272 +0,0 @@ |
||||
/* global */ |
||||
'use strict' |
||||
|
||||
var $ = require('jquery') |
||||
var yo = require('yo-yo') |
||||
var ethJSUtil = require('ethereumjs-util') |
||||
var BN = ethJSUtil.BN |
||||
var helper = require('../../lib/helper') |
||||
var copyToClipboard = require('./copy-to-clipboard') |
||||
var css = require('../../universal-dapp-styles') |
||||
var MultiParamManager = require('./multiParamManager') |
||||
var remixLib = require('@remix-project/remix-lib') |
||||
var txFormat = remixLib.execution.txFormat |
||||
const txHelper = remixLib.execution.txHelper |
||||
var TreeView = require('./TreeView') |
||||
var txCallBacks = require('./sendTxCallbacks') |
||||
const _paq = window._paq = window._paq || [] |
||||
|
||||
function UniversalDAppUI (blockchain, logCallback) { |
||||
this.blockchain = blockchain |
||||
this.logCallback = logCallback |
||||
this.compilerData = { contractsDetails: {} } |
||||
} |
||||
|
||||
function decodeResponseToTreeView (response, fnabi) { |
||||
var treeView = new TreeView({ |
||||
extractData: (item, parent, key) => { |
||||
var ret = {} |
||||
if (BN.isBN(item)) { |
||||
ret.self = item.toString(10) |
||||
ret.children = [] |
||||
} else { |
||||
ret = treeView.extractDataDefault(item, parent, key) |
||||
} |
||||
return ret |
||||
} |
||||
}) |
||||
return treeView.render(txFormat.decodeResponse(response, fnabi)) |
||||
} |
||||
|
||||
UniversalDAppUI.prototype.renderInstance = function (contract, address, contractName) { |
||||
var noInstances = document.querySelector('[data-id="deployAndRunNoInstanceText"]') |
||||
if (noInstances) { |
||||
noInstances.parentNode.removeChild(noInstances) |
||||
} |
||||
const abi = txHelper.sortAbiFunction(contract.abi) |
||||
return this.renderInstanceFromABI(abi, address, contractName, contract) |
||||
} |
||||
|
||||
// TODO this function was named before "appendChild".
|
||||
// this will render an instance: contract name, contract address, and all the public functions
|
||||
// basically this has to be called for the "atAddress" (line 393) and when a contract creation succeed
|
||||
// this returns a DOM element
|
||||
UniversalDAppUI.prototype.renderInstanceFromABI = function (contractABI, address, contractName, contract) { |
||||
const self = this |
||||
address = (address.slice(0, 2) === '0x' ? '' : '0x') + address.toString('hex') |
||||
address = ethJSUtil.toChecksumAddress(address) |
||||
var instance = yo`<div class="instance run-instance border-dark ${css.instance} ${css.hidesub}" id="instance${address}" data-shared="universalDappUiInstance"></div>` |
||||
const context = this.blockchain.context() |
||||
|
||||
var shortAddress = helper.shortenAddress(address) |
||||
var title = yo` |
||||
<div class="${css.title} alert alert-secondary"> |
||||
<button data-id="universalDappUiTitleExpander" class="btn ${css.titleExpander}" onclick="${(e) => { toggleClass(e) }}"> |
||||
<i class="fas fa-angle-right" aria-hidden="true"></i> |
||||
</button> |
||||
<div class="input-group ${css.nameNbuts}"> |
||||
<div class="${css.titleText} input-group-prepend"> |
||||
<span class="input-group-text ${css.spanTitleText}"> |
||||
${contractName} at ${shortAddress} (${context}) |
||||
</span> |
||||
</div> |
||||
<div class="btn-group"> |
||||
<button class="btn p-1 btn-secondary">${copyToClipboard(() => address)}</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
|
||||
var close = yo` |
||||
<button |
||||
class="${css.udappClose} mr-1 p-1 btn btn-secondary align-items-center" |
||||
data-id="universalDappUiUdappClose" |
||||
onclick=${remove} |
||||
title="Remove from the list" |
||||
> |
||||
<i class="${css.closeIcon} fas fa-times" aria-hidden="true"></i> |
||||
</button>` |
||||
title.querySelector('.btn-group').appendChild(close) |
||||
|
||||
var contractActionsWrapper = yo` |
||||
<div class="${css.cActionsWrapper}" data-id="universalDappUiContractActionWrapper"> |
||||
</div> |
||||
` |
||||
|
||||
function remove () { |
||||
instance.remove() |
||||
// @TODO perhaps add a callack here to warn the caller that the instance has been removed
|
||||
} |
||||
|
||||
function toggleClass (e) { |
||||
$(instance).toggleClass(`${css.hidesub} bg-light`) |
||||
// e.currentTarget.querySelector('i')
|
||||
e.currentTarget.querySelector('i').classList.toggle('fa-angle-right') |
||||
e.currentTarget.querySelector('i').classList.toggle('fa-angle-down') |
||||
} |
||||
|
||||
instance.appendChild(title) |
||||
instance.appendChild(contractActionsWrapper) |
||||
|
||||
$.each(contractABI, (i, funABI) => { |
||||
if (funABI.type !== 'function') { |
||||
return |
||||
} |
||||
// @todo getData cannot be used with overloaded functions
|
||||
contractActionsWrapper.appendChild(this.getCallButton({ |
||||
funABI: funABI, |
||||
address: address, |
||||
contractABI: contractABI, |
||||
contractName: contractName, |
||||
contract |
||||
})) |
||||
}) |
||||
|
||||
const calldataInput = yo` |
||||
<input id="deployAndRunLLTxCalldata" class="${css.calldataInput} form-control" title="The Calldata to send to fallback function of the contract."> |
||||
` |
||||
const llIError = yo` |
||||
<label id="deployAndRunLLTxError" class="text-danger my-2"></label> |
||||
` |
||||
// constract LLInteractions elements
|
||||
const lowLevelInteracions = yo` |
||||
<div class="d-flex flex-column"> |
||||
<div class="d-flex flex-row justify-content-between mt-2"> |
||||
<div class="py-2 border-top d-flex justify-content-start flex-grow-1"> |
||||
Low level interactions |
||||
</div> |
||||
<a |
||||
href="https://solidity.readthedocs.io/en/v0.6.2/contracts.html#receive-ether-function" |
||||
title="check out docs for using 'receive'/'fallback'" |
||||
target="_blank" |
||||
> |
||||
<i aria-hidden="true" class="fas fa-info my-2 mr-1"></i> |
||||
</a> |
||||
</div> |
||||
<div class="d-flex flex-column align-items-start"> |
||||
<label class="">CALLDATA</label> |
||||
<div class="d-flex justify-content-end w-100 align-items-center"> |
||||
${calldataInput} |
||||
<button id="deployAndRunLLTxSendTransaction" data-id="pluginManagerSettingsDeployAndRunLLTxSendTransaction" class="${css.instanceButton} p-0 w-50 btn border-warning text-warning" title="Send data to contract." onclick=${() => sendData()}>Transact</button> |
||||
</div> |
||||
</div> |
||||
<div> |
||||
${llIError} |
||||
</div> |
||||
</div> |
||||
` |
||||
|
||||
function sendData () { |
||||
function setLLIError (text) { |
||||
llIError.innerText = text |
||||
} |
||||
|
||||
setLLIError('') |
||||
const fallback = txHelper.getFallbackInterface(contractABI) |
||||
const receive = txHelper.getReceiveInterface(contractABI) |
||||
const args = { |
||||
funABI: fallback || receive, |
||||
address: address, |
||||
contractName: contractName, |
||||
contractABI: contractABI |
||||
} |
||||
const amount = document.querySelector('#value').value |
||||
if (amount !== '0') { |
||||
// check for numeric and receive/fallback
|
||||
if (!helper.isNumeric(amount)) { |
||||
return setLLIError('Value to send should be a number') |
||||
} else if (!receive && !(fallback && fallback.stateMutability === 'payable')) { |
||||
return setLLIError("In order to receive Ether transfer the contract should have either 'receive' or payable 'fallback' function") |
||||
} |
||||
} |
||||
let calldata = calldataInput.value |
||||
if (calldata) { |
||||
if (calldata.length < 4 && helper.is0XPrefixed(calldata)) { |
||||
return setLLIError('The calldata should be a valid hexadecimal value with size of at least one byte.') |
||||
} else { |
||||
if (helper.is0XPrefixed(calldata)) { |
||||
calldata = calldata.substr(2, calldata.length) |
||||
} |
||||
if (!helper.isHexadecimal(calldata)) { |
||||
return setLLIError('The calldata should be a valid hexadecimal value.') |
||||
} |
||||
} |
||||
if (!fallback) { |
||||
return setLLIError("'Fallback' function is not defined") |
||||
} |
||||
} |
||||
|
||||
if (!receive && !fallback) return setLLIError('Both \'receive\' and \'fallback\' functions are not defined') |
||||
|
||||
// we have to put the right function ABI:
|
||||
// if receive is defined and that there is no calldata => receive function is called
|
||||
// if fallback is defined => fallback function is called
|
||||
if (receive && !calldata) args.funABI = receive |
||||
else if (fallback) args.funABI = fallback |
||||
|
||||
if (!args.funABI) return setLLIError('Please define a \'Fallback\' function to send calldata and a either \'Receive\' or payable \'Fallback\' to send ethers') |
||||
self.runTransaction(false, args, null, calldataInput.value, null) |
||||
} |
||||
|
||||
contractActionsWrapper.appendChild(lowLevelInteracions) |
||||
return instance |
||||
} |
||||
|
||||
// TODO this is used by renderInstance when a new instance is displayed.
|
||||
// this returns a DOM element.
|
||||
UniversalDAppUI.prototype.getCallButton = function (args) { |
||||
const self = this |
||||
var outputOverride = yo`<div class=${css.value}></div>` // show return value
|
||||
const isConstant = args.funABI.constant !== undefined ? args.funABI.constant : false |
||||
const lookupOnly = args.funABI.stateMutability === 'view' || args.funABI.stateMutability === 'pure' || isConstant |
||||
const multiParamManager = new MultiParamManager( |
||||
lookupOnly, |
||||
args.funABI, |
||||
(valArray, inputsValues) => self.runTransaction(lookupOnly, args, valArray, inputsValues, outputOverride), |
||||
self.blockchain.getInputs(args.funABI) |
||||
) |
||||
|
||||
const contractActionsContainer = yo`<div class="${css.contractActionsContainer}" >${multiParamManager.render()}</div>` |
||||
contractActionsContainer.appendChild(outputOverride) |
||||
|
||||
return contractActionsContainer |
||||
} |
||||
|
||||
UniversalDAppUI.prototype.runTransaction = function (lookupOnly, args, valArr, inputsValues, outputOverride) { |
||||
const functionName = args.funABI.type === 'function' ? args.funABI.name : `(${args.funABI.type})` |
||||
const logMsg = `${lookupOnly ? 'call' : 'transact'} to ${args.contractName}.${functionName}` |
||||
|
||||
const callbacksInContext = txCallBacks.getCallBacksWithContext(this, this.blockchain) |
||||
|
||||
const outputCb = (returnValue) => { |
||||
if (outputOverride) { |
||||
const decoded = decodeResponseToTreeView(returnValue, args.funABI) |
||||
outputOverride.innerHTML = '' |
||||
outputOverride.appendChild(decoded) |
||||
} |
||||
} |
||||
let callinfo = '' |
||||
if (lookupOnly) callinfo = 'call' |
||||
else if (args.funABI.type === 'fallback' || args.funABI.type === 'receive') callinfo = 'lowLevelInteracions' |
||||
else callinfo = 'transact' |
||||
|
||||
_paq.push(['trackEvent', 'udapp', callinfo, this.blockchain.getCurrentNetworkStatus().network.name]) |
||||
const params = args.funABI.type !== 'fallback' ? inputsValues : '' |
||||
this.blockchain.runOrCallContractMethod( |
||||
args.contractName, |
||||
args.contractABI, |
||||
args.funABI, |
||||
args.contract, |
||||
inputsValues, |
||||
args.address, |
||||
params, |
||||
lookupOnly, |
||||
logMsg, |
||||
this.logCallback, |
||||
outputCb, |
||||
callbacksInContext.confirmationCb.bind(callbacksInContext), |
||||
callbacksInContext.continueCb.bind(callbacksInContext), |
||||
callbacksInContext.promptCb.bind(callbacksInContext)) |
||||
} |
||||
|
||||
module.exports = UniversalDAppUI |
@ -0,0 +1,13 @@ |
||||
const transactionDetailsLinks = { |
||||
Main: 'https://www.etherscan.io/tx/', |
||||
Rinkeby: 'https://rinkeby.etherscan.io/tx/', |
||||
Ropsten: 'https://ropsten.etherscan.io/tx/', |
||||
Kovan: 'https://kovan.etherscan.io/tx/', |
||||
Goerli: 'https://goerli.etherscan.io/tx/' |
||||
} |
||||
|
||||
export function etherScanLink (network: string, hash: string): string { |
||||
if (transactionDetailsLinks[network]) { |
||||
return transactionDetailsLinks[network] + hash |
||||
} |
||||
} |
@ -1,34 +0,0 @@ |
||||
export class FramingService { |
||||
constructor (sidePanel, verticalIcons, mainView, resizeFeature) { |
||||
this.sidePanel = sidePanel |
||||
this.verticalIcons = verticalIcons |
||||
this.mainPanel = mainView.getAppPanel() |
||||
this.mainView = mainView |
||||
this.resizeFeature = resizeFeature |
||||
} |
||||
|
||||
start (params) { |
||||
this.verticalIcons.select('filePanel') |
||||
|
||||
document.addEventListener('keypress', (e) => { |
||||
if (e.shiftKey && e.ctrlKey) { |
||||
if (e.code === 'KeyF') { // Ctrl+Shift+F
|
||||
this.verticalIcons.select('filePanel') |
||||
} else if (e.code === 'KeyA') { // Ctrl+Shift+A
|
||||
this.verticalIcons.select('pluginManager') |
||||
} else if (e.code === 'KeyS') { // Ctrl+Shift+S
|
||||
this.verticalIcons.select('settings') |
||||
} |
||||
e.preventDefault() |
||||
} |
||||
}) |
||||
|
||||
if (params.minimizeterminal) this.mainView.minimizeTerminal() |
||||
if (params.minimizesidepanel) this.resizeFeature.hidePanel() |
||||
} |
||||
|
||||
embed () { |
||||
this.mainView.minimizeTerminal() |
||||
this.resizeFeature.hidePanel() |
||||
} |
||||
} |
@ -1,135 +0,0 @@ |
||||
'use strict' |
||||
import { CompilerImports } from '@remix-project/core-plugin' |
||||
import Registry from '../app/state/registry' |
||||
var yo = require('yo-yo') |
||||
var async = require('async') |
||||
var EventManager = require('../lib/events') |
||||
|
||||
var toolTip = require('../app/ui/tooltip') |
||||
var GistHandler = require('./gist-handler') |
||||
|
||||
class CmdInterpreterAPI { |
||||
constructor (terminal, blockchain) { |
||||
const self = this |
||||
self.event = new EventManager() |
||||
self.blockchain = blockchain |
||||
self._components = {} |
||||
self._components.registry = Registry.getInstance() |
||||
self._components.terminal = terminal |
||||
self._components.fileImport = new CompilerImports() |
||||
self._components.gistHandler = new GistHandler() |
||||
self._deps = { |
||||
fileManager: self._components.registry.get('filemanager').api, |
||||
editor: self._components.registry.get('editor').api, |
||||
compilersArtefacts: self._components.registry.get('compilersartefacts').api, |
||||
offsetToLineColumnConverter: self._components.registry.get('offsettolinecolumnconverter').api |
||||
} |
||||
self.commandHelp = { |
||||
'remix.loadgist(id)': 'Load a gist in the file explorer.', |
||||
'remix.loadurl(url)': 'Load the given url in the file explorer. The url can be of type github, swarm, ipfs or raw http', |
||||
'remix.execute(filepath)': 'Run the script specified by file path. If filepath is empty, script currently displayed in the editor is executed.', |
||||
'remix.exeCurrent()': 'Run the script currently displayed in the editor', |
||||
'remix.help()': 'Display this help message' |
||||
} |
||||
} |
||||
|
||||
log () { arguments[0] != null ? this._components.terminal.commands.html(arguments[0]) : this._components.terminal.commands.html(arguments[1]) } |
||||
loadgist (id, cb) { |
||||
const self = this |
||||
self._components.gistHandler.loadFromGist({ gist: id }, this._deps.fileManager) |
||||
if (cb) cb() |
||||
} |
||||
|
||||
loadurl (url, cb) { |
||||
const self = this |
||||
self._components.fileImport.import(url, |
||||
(loadingMsg) => { toolTip(loadingMsg) }, |
||||
(err, content, cleanUrl, type, url) => { |
||||
if (err) { |
||||
toolTip(`Unable to load ${url}: ${err}`) |
||||
if (cb) cb(err) |
||||
} else { |
||||
self._deps.fileManager.writeFile(type + '/' + cleanUrl, content) |
||||
try { |
||||
content = JSON.parse(content) |
||||
async.eachOfSeries(content.sources, (value, file, callbackSource) => { |
||||
var url = value.urls[0] // @TODO retrieve all other contents ?
|
||||
self._components.fileImport.import(url, |
||||
(loadingMsg) => { toolTip(loadingMsg) }, |
||||
async (error, content, cleanUrl, type, url) => { |
||||
if (error) { |
||||
toolTip(`Cannot retrieve the content of ${url}: ${error}`) |
||||
return callbackSource(`Cannot retrieve the content of ${url}: ${error}`) |
||||
} else { |
||||
try { |
||||
await self._deps.fileManager.writeFile(type + '/' + cleanUrl, content) |
||||
callbackSource() |
||||
} catch (e) { |
||||
callbackSource(e.message) |
||||
} |
||||
} |
||||
}) |
||||
}, (error) => { |
||||
if (cb) cb(error) |
||||
}) |
||||
} catch (e) {} |
||||
if (cb) cb() |
||||
} |
||||
}) |
||||
} |
||||
|
||||
exeCurrent (cb) { |
||||
return this.execute(undefined, cb) |
||||
} |
||||
|
||||
execute (file, cb) { |
||||
const self = this |
||||
|
||||
function _execute (content, cb) { |
||||
if (!content) { |
||||
toolTip('no content to execute') |
||||
if (cb) cb() |
||||
return |
||||
} |
||||
self._components.terminal.commands.script(content) |
||||
} |
||||
|
||||
if (typeof file === 'undefined') { |
||||
var content = self._deps.editor.currentContent() |
||||
_execute(content, cb) |
||||
return |
||||
} |
||||
|
||||
var provider = self._deps.fileManager.fileProviderOf(file) |
||||
|
||||
if (!provider) { |
||||
toolTip(`provider for path ${file} not found`) |
||||
if (cb) cb() |
||||
return |
||||
} |
||||
|
||||
provider.get(file, (error, content) => { |
||||
if (error) { |
||||
toolTip(error) |
||||
if (cb) cb() |
||||
return |
||||
} |
||||
|
||||
_execute(content, cb) |
||||
}) |
||||
} |
||||
|
||||
help (cb) { |
||||
const self = this |
||||
var help = yo`<div></div>` |
||||
for (var k in self.commandHelp) { |
||||
help.appendChild(yo`<div>${k}: ${self.commandHelp[k]}</div>`) |
||||
help.appendChild(yo`<br>`) |
||||
} |
||||
self._components.terminal.commands.html(help) |
||||
if (cb) cb() |
||||
return '' |
||||
} |
||||
} |
||||
|
||||
module.exports = CmdInterpreterAPI |
@ -1,74 +0,0 @@ |
||||
'use strict' |
||||
var modalDialogCustom = require('../app/ui/modal-dialog-custom') |
||||
var request = require('request') |
||||
|
||||
// Allowing window to be overriden for testing
|
||||
function GistHandler (_window) { |
||||
if (_window !== undefined) { |
||||
modalDialogCustom = _window |
||||
} |
||||
|
||||
this.handleLoad = function (params, cb) { |
||||
if (!cb) cb = () => {} |
||||
var loadingFromGist = false |
||||
var gistId |
||||
if (params.gist === '') { |
||||
loadingFromGist = true |
||||
modalDialogCustom.prompt('Load a Gist', 'Enter the ID of the Gist or URL you would like to load.', null, (target) => { |
||||
if (target !== '') { |
||||
gistId = getGistId(target) |
||||
if (gistId) { |
||||
cb(gistId) |
||||
} else { |
||||
modalDialogCustom.alert('Gist load error', 'Error while loading gist. Please provide a valid Gist ID or URL.') |
||||
} |
||||
} |
||||
}) |
||||
return loadingFromGist |
||||
} else { |
||||
gistId = params.gist |
||||
loadingFromGist = !!gistId |
||||
} |
||||
if (loadingFromGist) { |
||||
cb(gistId) |
||||
} |
||||
return loadingFromGist |
||||
} |
||||
|
||||
function getGistId (str) { |
||||
var idr = /[0-9A-Fa-f]{8,}/ |
||||
var match = idr.exec(str) |
||||
return match ? match[0] : null |
||||
} |
||||
|
||||
this.loadFromGist = (params, fileManager) => { |
||||
const self = this |
||||
return self.handleLoad(params, function (gistId) { |
||||
request.get({ |
||||
url: `https://api.github.com/gists/${gistId}`, |
||||
json: true |
||||
}, async (error, response, data = {}) => { |
||||
if (error || !data.files) { |
||||
modalDialogCustom.alert('Gist load error', error || data.message) |
||||
return |
||||
} |
||||
const obj = {} |
||||
Object.keys(data.files).forEach((element) => { |
||||
const path = element.replace(/\.\.\./g, '/') |
||||
|
||||
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element] |
||||
}) |
||||
fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => { |
||||
if (!errorLoadingFile) { |
||||
const provider = fileManager.getProvider('workspace') |
||||
provider.lastLoadedGistId = gistId |
||||
} else { |
||||
modalDialogCustom.alert('Gist load error', errorLoadingFile.message || errorLoadingFile) |
||||
} |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
module.exports = GistHandler |
@ -1,88 +0,0 @@ |
||||
const yo = require('yo-yo') |
||||
const csjs = require('csjs-inject') |
||||
|
||||
const css = csjs` |
||||
.dragbar { |
||||
width : 2px; |
||||
height : 100%; |
||||
cursor : col-resize; |
||||
z-index : 999; |
||||
} |
||||
.ghostbar { |
||||
width : 3px; |
||||
background-color : var(--primary); |
||||
opacity : 0.5; |
||||
position : absolute; |
||||
cursor : col-resize; |
||||
z-index : 9999; |
||||
top : 0; |
||||
bottom : 0; |
||||
} |
||||
` |
||||
|
||||
export default class PanelsResize { |
||||
constructor (panel) { |
||||
this.panel = panel |
||||
const string = panel.style.minWidth |
||||
this.minWidth = string.length > 2 ? parseInt(string.substring(0, string.length - 2)) : 0 |
||||
} |
||||
|
||||
render () { |
||||
this.ghostbar = yo`<div class=${css.ghostbar}></div>` |
||||
|
||||
const mousedown = (event) => { |
||||
event.preventDefault() |
||||
if (event.which === 1) { |
||||
moveGhostbar(event) |
||||
document.body.appendChild(this.ghostbar) |
||||
document.addEventListener('mousemove', moveGhostbar) |
||||
document.addEventListener('mouseup', removeGhostbar) |
||||
document.addEventListener('keydown', cancelGhostbar) |
||||
} |
||||
} |
||||
|
||||
const cancelGhostbar = (event) => { |
||||
if (event.keyCode === 27) { |
||||
document.body.removeChild(this.ghostbar) |
||||
document.removeEventListener('mousemove', moveGhostbar) |
||||
document.removeEventListener('mouseup', removeGhostbar) |
||||
document.removeEventListener('keydown', cancelGhostbar) |
||||
} |
||||
} |
||||
|
||||
const moveGhostbar = (event) => { |
||||
this.ghostbar.style.left = event.x + 'px' |
||||
} |
||||
|
||||
const removeGhostbar = (event) => { |
||||
document.body.removeChild(this.ghostbar) |
||||
document.removeEventListener('mousemove', moveGhostbar) |
||||
document.removeEventListener('mouseup', removeGhostbar) |
||||
document.removeEventListener('keydown', cancelGhostbar) |
||||
this.setPosition(event) |
||||
} |
||||
|
||||
return yo`<div onmousedown=${mousedown} class=${css.dragbar}></div>` |
||||
} |
||||
|
||||
calculatePanelWidth (event) { |
||||
return event.x - this.panel.offsetLeft |
||||
} |
||||
|
||||
setPosition (event) { |
||||
const panelWidth = this.calculatePanelWidth(event) |
||||
// close the panel if the width is less than a minWidth
|
||||
if (panelWidth > this.minWidth - 10 || this.panel.style.display === 'none') { |
||||
this.panel.style.width = panelWidth + 'px' |
||||
this.showPanel() |
||||
} else this.hidePanel() |
||||
} |
||||
|
||||
hidePanel () { |
||||
this.panel.style.display = 'none' |
||||
} |
||||
|
||||
showPanel () { |
||||
this.panel.style.display = 'flex' |
||||
} |
||||
} |
@ -1,121 +0,0 @@ |
||||
'use strict' |
||||
|
||||
const async = require('async') |
||||
const IpfsClient = require('ipfs-mini') |
||||
|
||||
const ipfsNodes = [ |
||||
new IpfsClient({ host: 'ipfs.remixproject.org', port: 443, protocol: 'https' }), |
||||
new IpfsClient({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' }), |
||||
new IpfsClient({ host: '127.0.0.1', port: 5001, protocol: 'http' }) |
||||
] |
||||
|
||||
module.exports = (contract, fileManager, cb, ipfsVerifiedPublishCallBack) => { |
||||
// gather list of files to publish
|
||||
var sources = [] |
||||
|
||||
var metadata |
||||
|
||||
try { |
||||
metadata = JSON.parse(contract.metadata) |
||||
} catch (e) { |
||||
return cb(e) |
||||
} |
||||
|
||||
if (metadata === undefined) { |
||||
return cb('No metadata') |
||||
} |
||||
|
||||
async.eachSeries(Object.keys(metadata.sources), function (fileName, cb) { |
||||
// find hash
|
||||
let hash = null |
||||
try { |
||||
// we try extract the hash defined in the metadata.json
|
||||
// in order to check if the hash that we get after publishing is the same as the one located in metadata.json
|
||||
// if it's not the same, we throw "hash mismatch between solidity bytecode and uploaded content"
|
||||
// if we don't find the hash in the metadata.json, the check is not done.
|
||||
//
|
||||
// TODO: refactor this with publishOnSwarm
|
||||
if (metadata.sources[fileName].urls) { |
||||
metadata.sources[fileName].urls.forEach(url => { |
||||
if (url.includes('ipfs')) hash = url.match('dweb:/ipfs/(.+)')[1] |
||||
}) |
||||
} |
||||
} catch (e) { |
||||
return cb('Error while extracting the hash from metadata.json') |
||||
} |
||||
|
||||
fileManager.fileProviderOf(fileName).get(fileName, (error, content) => { |
||||
if (error) { |
||||
console.log(error) |
||||
} else { |
||||
sources.push({ |
||||
content: content, |
||||
hash: hash, |
||||
filename: fileName |
||||
}) |
||||
} |
||||
cb() |
||||
}) |
||||
}, function (error) { |
||||
if (error) { |
||||
cb(error) |
||||
} else { |
||||
// publish the list of sources in order, fail if any failed
|
||||
var uploaded = [] |
||||
async.eachSeries(sources, function (item, cb) { |
||||
ipfsVerifiedPublish(item.content, item.hash, (error, result) => { |
||||
try { |
||||
item.hash = result.url.match('dweb:/ipfs/(.+)')[1] |
||||
} catch (e) { |
||||
item.hash = '<Metadata inconsistency> - ' + item.fileName |
||||
} |
||||
if (!error && ipfsVerifiedPublishCallBack) ipfsVerifiedPublishCallBack(item) |
||||
item.output = result |
||||
uploaded.push(item) |
||||
cb(error) |
||||
}) |
||||
}, () => { |
||||
const metadataContent = JSON.stringify(metadata) |
||||
ipfsVerifiedPublish(metadataContent, '', (error, result) => { |
||||
try { |
||||
contract.metadataHash = result.url.match('dweb:/ipfs/(.+)')[1] |
||||
} catch (e) { |
||||
contract.metadataHash = '<Metadata inconsistency> - metadata.json' |
||||
} |
||||
if (!error && ipfsVerifiedPublishCallBack) { |
||||
ipfsVerifiedPublishCallBack({ |
||||
content: metadataContent, |
||||
hash: contract.metadataHash |
||||
}) |
||||
} |
||||
uploaded.push({ |
||||
content: contract.metadata, |
||||
hash: contract.metadataHash, |
||||
filename: 'metadata.json', |
||||
output: result |
||||
}) |
||||
cb(error, uploaded) |
||||
}) |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
async function ipfsVerifiedPublish (content, expectedHash, cb) { |
||||
try { |
||||
const results = await severalGatewaysPush(content) |
||||
if (expectedHash && results !== expectedHash) { |
||||
cb(null, { message: 'hash mismatch between solidity bytecode and uploaded content.', url: 'dweb:/ipfs/' + results, hash: results }) |
||||
} else { |
||||
cb(null, { message: 'ok', url: 'dweb:/ipfs/' + results, hash: results }) |
||||
} |
||||
} catch (error) { |
||||
cb(error) |
||||
} |
||||
} |
||||
|
||||
function severalGatewaysPush (content) { |
||||
const invert = p => new Promise((resolve, reject) => p.then(reject).catch(resolve)) // Invert res and rej
|
||||
const promises = ipfsNodes.map((node) => invert(node.add(content))) |
||||
return invert(Promise.all(promises)) |
||||
} |
@ -1,109 +0,0 @@ |
||||
'use strict' |
||||
|
||||
var async = require('async') |
||||
var swarmgw = require('swarmgw')() |
||||
|
||||
module.exports = (contract, fileManager, cb, swarmVerifiedPublishCallBack) => { |
||||
// gather list of files to publish
|
||||
var sources = [] |
||||
|
||||
var metadata |
||||
try { |
||||
metadata = JSON.parse(contract.metadata) |
||||
} catch (e) { |
||||
return cb(e) |
||||
} |
||||
|
||||
if (metadata === undefined) { |
||||
return cb('No metadata') |
||||
} |
||||
|
||||
async.eachSeries(Object.keys(metadata.sources), function (fileName, cb) { |
||||
// find hash
|
||||
let hash = null |
||||
try { |
||||
// we try extract the hash defined in the metadata.json
|
||||
// in order to check if the hash that we get after publishing is the same as the one located in metadata.json
|
||||
// if it's not the same, we throw "hash mismatch between solidity bytecode and uploaded content"
|
||||
// if we don't find the hash in the metadata.json, the check is not done.
|
||||
//
|
||||
// TODO: refactor this with publishOnIpfs
|
||||
if (metadata.sources[fileName].urls) { |
||||
metadata.sources[fileName].urls.forEach(url => { |
||||
if (url.includes('bzz')) hash = url.match('(bzzr|bzz-raw)://(.+)')[1] |
||||
}) |
||||
} |
||||
} catch (e) { |
||||
return cb('Error while extracting the hash from metadata.json') |
||||
} |
||||
|
||||
fileManager.fileProviderOf(fileName).get(fileName, (error, content) => { |
||||
if (error) { |
||||
console.log(error) |
||||
} else { |
||||
sources.push({ |
||||
content: content, |
||||
hash: hash, |
||||
filename: fileName |
||||
}) |
||||
} |
||||
cb() |
||||
}) |
||||
}, function (error) { |
||||
if (error) { |
||||
cb(error) |
||||
} else { |
||||
// publish the list of sources in order, fail if any failed
|
||||
var uploaded = [] |
||||
async.eachSeries(sources, function (item, cb) { |
||||
swarmVerifiedPublish(item.content, item.hash, (error, result) => { |
||||
try { |
||||
item.hash = result.url.match('bzz-raw://(.+)')[1] |
||||
} catch (e) { |
||||
item.hash = '<Metadata inconsistency> - ' + item.fileName |
||||
} |
||||
if (!error && swarmVerifiedPublishCallBack) swarmVerifiedPublishCallBack(item) |
||||
item.output = result |
||||
uploaded.push(item) |
||||
// TODO this is a fix cause Solidity metadata does not contain the right swarm hash (poc 0.3)
|
||||
metadata.sources[item.filename].urls[0] = result.url |
||||
cb(error) |
||||
}) |
||||
}, () => { |
||||
const metadataContent = JSON.stringify(metadata) |
||||
swarmVerifiedPublish(metadataContent, '', (error, result) => { |
||||
try { |
||||
contract.metadataHash = result.url.match('bzz-raw://(.+)')[1] |
||||
} catch (e) { |
||||
contract.metadataHash = '<Metadata inconsistency> - metadata.json' |
||||
} |
||||
if (!error && swarmVerifiedPublishCallBack) { |
||||
swarmVerifiedPublishCallBack({ |
||||
content: metadataContent, |
||||
hash: contract.metadataHash |
||||
}) |
||||
} |
||||
uploaded.push({ |
||||
content: contract.metadata, |
||||
hash: contract.metadataHash, |
||||
filename: 'metadata.json', |
||||
output: result |
||||
}) |
||||
cb(error, uploaded) |
||||
}) |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function swarmVerifiedPublish (content, expectedHash, cb) { |
||||
swarmgw.put(content, function (err, ret) { |
||||
if (err) { |
||||
cb(err) |
||||
} else if (expectedHash && ret !== expectedHash) { |
||||
cb(null, { message: 'hash mismatch between solidity bytecode and uploaded content.', url: 'bzz-raw://' + ret, hash: ret }) |
||||
} else { |
||||
cb(null, { message: 'ok', url: 'bzz-raw://' + ret, hash: ret }) |
||||
} |
||||
}) |
||||
} |
@ -1,145 +0,0 @@ |
||||
'use strict' |
||||
var EventManager = require('../lib/events') |
||||
var modalDialog = require('../app/ui/modaldialog') |
||||
var yo = require('yo-yo') |
||||
|
||||
class Remixd { |
||||
constructor (port) { |
||||
this.event = new EventManager() |
||||
this.port = port |
||||
this.callbacks = {} |
||||
this.callid = 0 |
||||
this.socket = null |
||||
this.connected = false |
||||
this.receiveResponse() |
||||
} |
||||
|
||||
online () { |
||||
return this.socket !== null |
||||
} |
||||
|
||||
close () { |
||||
if (this.socket) { |
||||
this.socket.close() |
||||
this.socket = null |
||||
} |
||||
} |
||||
|
||||
start (cb) { |
||||
if (this.socket) { |
||||
try { |
||||
this.socket.close() |
||||
} catch (e) {} |
||||
} |
||||
this.event.trigger('connecting', []) |
||||
this.socket = new WebSocket('ws://localhost:' + this.port, 'echo-protocol') // eslint-disable-line
|
||||
|
||||
this.socket.addEventListener('open', (event) => { |
||||
this.connected = true |
||||
this.event.trigger('connected', [event]) |
||||
cb() |
||||
}) |
||||
|
||||
this.socket.addEventListener('message', (event) => { |
||||
var data = JSON.parse(event.data) |
||||
if (data.type === 'reply') { |
||||
if (this.callbacks[data.id]) { |
||||
this.callbacks[data.id](data.error, data.result) |
||||
delete this.callbacks[data.id] |
||||
} |
||||
this.event.trigger('replied', [data]) |
||||
} else if (data.type === 'notification') { |
||||
this.event.trigger('notified', [data]) |
||||
} else if (data.type === 'system') { |
||||
if (data.error) { |
||||
this.event.trigger('system', [{ |
||||
error: data.error |
||||
}]) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
this.socket.addEventListener('error', (event) => { |
||||
this.errored(event) |
||||
cb(event) |
||||
}) |
||||
|
||||
this.socket.addEventListener('close', (event) => { |
||||
if (event.wasClean) { |
||||
this.connected = false |
||||
this.event.trigger('closed', [event]) |
||||
} else { |
||||
this.errored(event) |
||||
} |
||||
this.socket = null |
||||
}) |
||||
} |
||||
|
||||
async receiveResponse (requestId) { |
||||
return new Promise((resolve, reject) => { |
||||
this.event.register('replied', (data) => { |
||||
if (data.id === requestId) { |
||||
if (data.error) reject(data.error) |
||||
else resolve(data.result) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
errored (event) { |
||||
function remixdDialog () { |
||||
return yo`<div>Connection to Remixd closed. Localhost connection not available anymore.</div>` |
||||
} |
||||
if (this.connected) { |
||||
modalDialog('Lost connection to Remixd!', remixdDialog(), {}, { label: '' }) |
||||
} |
||||
this.connected = false |
||||
this.socket = null |
||||
this.event.trigger('errored', [event]) |
||||
} |
||||
|
||||
call (service, fn, args, callback) { |
||||
return new Promise((resolve, reject) => { |
||||
this.ensureSocket((error) => { |
||||
if (error) { |
||||
callback && typeof callback === 'function' && callback(error) |
||||
reject(error) |
||||
return |
||||
} |
||||
if (this.socket && this.socket.readyState === this.socket.OPEN) { |
||||
var data = this.format(service, fn, args) |
||||
this.callbacks[data.id] = callback |
||||
this.socket.send(JSON.stringify(data)) |
||||
resolve(data.id) |
||||
} else { |
||||
callback && typeof callback === 'function' && callback('Socket not ready. state:' + this.socket.readyState) |
||||
reject(new Error('Socket not ready. state:' + this.socket.readyState)) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
ensureSocket (cb) { |
||||
if (this.socket) return cb(null, this.socket) |
||||
this.start((error) => { |
||||
if (error) { |
||||
cb(error) |
||||
} else { |
||||
cb(null, this.socket) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
format (service, fn, args) { |
||||
var data = { |
||||
id: this.callid, |
||||
service: service, |
||||
fn: fn, |
||||
args: args |
||||
} |
||||
this.callid++ |
||||
return data |
||||
} |
||||
} |
||||
|
||||
module.exports = Remixd |
@ -1,64 +0,0 @@ |
||||
import { Storage } from '@remix-project/remix-lib' |
||||
import { joinPath } from './lib/helper' |
||||
import yo from 'yo-yo' |
||||
const modalDialogCustom = require('./app/ui/modal-dialog-custom') |
||||
/* |
||||
Migrating the files to the BrowserFS storage instead or raw localstorage |
||||
*/ |
||||
export default (fileProvider) => { |
||||
const fileStorage = new Storage('sol:') |
||||
const flag = 'status' |
||||
const fileStorageBrowserFS = new Storage('remix_browserFS_migration:') |
||||
if (fileStorageBrowserFS.get(flag) === 'done') return |
||||
fileStorage.keys().forEach((path) => { |
||||
if (path !== '.remix.config') { |
||||
const content = fileStorage.get(path) |
||||
fileProvider.set(path, content) |
||||
// TODO https://github.com/ethereum/remix-ide/issues/2377
|
||||
// fileStorage.remove(path) we don't want to remove it as we are still supporting the old version
|
||||
} |
||||
}) |
||||
fileStorageBrowserFS.set(flag, 'done') |
||||
} |
||||
|
||||
export async function migrateToWorkspace (fileManager, filePanel) { |
||||
const browserProvider = fileManager.getProvider('browser') |
||||
const workspaceProvider = fileManager.getProvider('workspace') |
||||
const files = await browserProvider.copyFolderToJson('/') |
||||
|
||||
if (Object.keys(files).length === 0) { |
||||
// we don't have any root file, only .workspaces
|
||||
// don't need to create a workspace
|
||||
throw new Error('No file to migrate') |
||||
} |
||||
|
||||
if (Object.keys(files).length === 1 && files['/.workspaces']) { |
||||
// we don't have any root file, only .workspaces
|
||||
// don't need to create a workspace
|
||||
throw new Error('No file to migrate') |
||||
} |
||||
|
||||
const workspaceName = 'workspace_migrated_' + Date.now() |
||||
await filePanel.processCreateWorkspace(workspaceName) |
||||
filePanel.getWorkspaces() // refresh list
|
||||
const workspacePath = joinPath('browser', workspaceProvider.workspacesPath, workspaceName) |
||||
await populateWorkspace(workspacePath, files, browserProvider) |
||||
return workspaceName |
||||
} |
||||
|
||||
const populateWorkspace = async (workspace, json, browserProvider) => { |
||||
for (const item in json) { |
||||
const isFolder = json[item].content === undefined |
||||
if (isFolder && item === '/.workspaces') continue // we don't want to replicate this one.
|
||||
if (isFolder) { |
||||
browserProvider.createDir(joinPath(workspace, item)) |
||||
await populateWorkspace(workspace, json[item].children, browserProvider) |
||||
} else { |
||||
await browserProvider.set(joinPath(workspace, item), json[item].content, (err) => { |
||||
if (err && err.message) { |
||||
modalDialogCustom.alert(yo`<div>There was an error migrating your files:${err.message} <div>Please use the ‘Download all Files' action, clear the local storage and re-import your files manually or use the 'Restore files' action.</div></div>`) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
} |
@ -1,47 +0,0 @@ |
||||
const yo = require('yo-yo') |
||||
const publishOnSwarm = require('./lib/publishOnSwarm') |
||||
const publishOnIpfs = require('./lib/publishOnIpfs') |
||||
const modalDialogCustom = require('./app/ui/modal-dialog-custom') |
||||
|
||||
export default function publish (storage, fileProvider, fileManager, contract) { |
||||
if (contract) { |
||||
if (contract.metadata === undefined || contract.metadata.length === 0) { |
||||
modalDialogCustom.alert('This contract may be abstract, may not implement an abstract parent\'s methods completely or not invoke an inherited contract\'s constructor correctly.') |
||||
} else { |
||||
if (storage === 'swarm') { |
||||
publishOnSwarm(contract, fileManager, function (err, uploaded) { |
||||
if (err) { |
||||
try { |
||||
err = JSON.stringify(err) |
||||
} catch (e) {} |
||||
console.log(`Failed to publish metadata file to swarm, please check the Swarm gateways is available ( swarm-gateways.net ) ${err}`) |
||||
} else { |
||||
var result = yo`<div>${uploaded.map((value) => { |
||||
return yo`<div><b>${value.filename}</b> : <pre>${value.output.url}</pre></div>` |
||||
})}</div>` |
||||
modalDialogCustom.alert(`Published ${contract.name}'s Metadata`, yo`<span>Metadata of "${contract.name.toLowerCase()}" was published successfully.<br> <pre>${result}</pre> </span>`) |
||||
} |
||||
}, (item) => { // triggered each time there's a new verified publish (means hash correspond)
|
||||
fileProvider.addExternal('swarm/' + item.hash, item.content) |
||||
}) |
||||
} else { |
||||
publishOnIpfs(contract, fileManager, function (err, uploaded) { |
||||
if (err) { |
||||
try { |
||||
err = JSON.stringify(err) |
||||
} catch (e) {} |
||||
modalDialogCustom.alert(yo`<span>Failed to publish metadata file to ${storage}, please check the ${storage} gateways is available.<br />
|
||||
${err}</span>`) |
||||
} else { |
||||
var result = yo`<div>${uploaded.map((value) => { |
||||
return yo`<div><b>${value.filename}</b> : <pre>${value.output.url.replace('dweb:/ipfs/', 'ipfs://')}</pre></div>` |
||||
})}</div>` |
||||
modalDialogCustom.alert(`Published ${contract.name}'s Metadata`, yo`<span>Metadata of "${contract.name.toLowerCase()}" was published successfully.<br> <pre>${result}</pre> </span>`) |
||||
} |
||||
}, (item) => { // triggered each time there's a new verified publish (means hash correspond)
|
||||
fileProvider.addExternal('ipfs/' + item.hash, item.content) |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,285 +0,0 @@ |
||||
const csjs = require('csjs-inject') |
||||
|
||||
var css = csjs` |
||||
.instanceTitleContainer { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.calldataInput{ |
||||
height: 32px; |
||||
} |
||||
.title { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
font-size: 11px; |
||||
width: 100%; |
||||
overflow: hidden; |
||||
word-break: break-word; |
||||
line-height: initial; |
||||
overflow: visible; |
||||
padding: 0 0 8px; |
||||
margin: 0; |
||||
background: none; |
||||
border: none; |
||||
} |
||||
.title button { |
||||
background: none; |
||||
border: none; |
||||
} |
||||
.titleLine { |
||||
display: flex; |
||||
align-items: baseline; |
||||
} |
||||
.titleText { |
||||
word-break: break-word; |
||||
width: 100%; |
||||
border: none; |
||||
overflow: hidden; |
||||
} |
||||
.spanTitleText { |
||||
line-height: 12px; |
||||
padding: 0; |
||||
font-size: 11px; |
||||
width:100%; |
||||
border: none; |
||||
background: none; |
||||
text-transform: uppercase; |
||||
overflow: hidden; |
||||
} |
||||
.inputGroupText { |
||||
width: 100%; |
||||
} |
||||
.title .copy { |
||||
color: var(--primary); |
||||
} |
||||
.titleExpander { |
||||
padding: 5px 7px; |
||||
} |
||||
.nameNbuts { |
||||
display: contents; |
||||
flex-wrap: nowrap; |
||||
width: 100%; |
||||
} |
||||
.instance { |
||||
display: block; |
||||
flex-direction: column; |
||||
margin-bottom: 12px; |
||||
background: none; |
||||
border-radius: 2px; |
||||
} |
||||
.instance.hidesub { |
||||
border-bottom: 1px solid; |
||||
} |
||||
.instance.hidesub .title { |
||||
display: flex; |
||||
} |
||||
.instance.hidesub .udappClose { |
||||
display: flex; |
||||
} |
||||
.instance.hidesub > * { |
||||
display: none; |
||||
} |
||||
.methCaret { |
||||
min-width: 12px; |
||||
width: 12px; |
||||
margin-left: 4px; |
||||
cursor: pointer; |
||||
font-size: 16px; |
||||
line-height: 0.6; |
||||
vertical-align: middle; |
||||
padding: 0; |
||||
} |
||||
.cActionsWrapper { |
||||
border-top-left-radius: 0; |
||||
border-bottom-left-radius: 0.25rem; |
||||
border-top-rightt-radius: 0; |
||||
border-bottom-right-radius: 0.25rem; |
||||
padding: 8px 10px 7px; |
||||
} |
||||
.group:after { |
||||
content: ""; |
||||
display: table; |
||||
clear: both; |
||||
} |
||||
.buttonsContainer { |
||||
margin-top: 2%; |
||||
display: flex; |
||||
overflow: hidden; |
||||
} |
||||
.instanceButton { |
||||
height: 32px; |
||||
border-radius: 3px; |
||||
white-space: nowrap; |
||||
font-size: 11px; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
.closeIcon { |
||||
font-size: 12px; |
||||
cursor: pointer; |
||||
margin-left: 5px; |
||||
} |
||||
.udappClose { |
||||
display: flex; |
||||
justify-content: flex-end; |
||||
} |
||||
.contractProperty { |
||||
width:100%; |
||||
} |
||||
.contractProperty.hasArgs input { |
||||
padding: .36em; |
||||
border-radius: 5px; |
||||
} |
||||
.contractProperty .contractActionsContainerSingle input{ |
||||
border-top-left-radius: 0; |
||||
border-bottom-left-radius: 0; |
||||
} |
||||
.contractProperty button { |
||||
min-width: 100px; |
||||
width: 100px; |
||||
margin:0; |
||||
word-break: inherit; |
||||
} |
||||
.contractProperty button:disabled { |
||||
cursor: not-allowed; |
||||
background-color: white; |
||||
border-color: lightgray; |
||||
} |
||||
.contractProperty.constant button { |
||||
min-width: 100px; |
||||
width: 100px; |
||||
margin:0; |
||||
word-break: inherit; |
||||
outline: none; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
.contractProperty > .value { |
||||
box-sizing: border-box; |
||||
float: left; |
||||
align-self: center; |
||||
margin-left: 4px; |
||||
} |
||||
.contractActionsContainer { |
||||
width: 100%; |
||||
margin-bottom: 8px; |
||||
} |
||||
.contractActionsContainerSingle { |
||||
display: flex; |
||||
width: 100%; |
||||
} |
||||
.contractActionsContainerSingle i { |
||||
line-height: 2; |
||||
} |
||||
.contractActionsContainerMulti { |
||||
display:none; |
||||
width: 100%; |
||||
} |
||||
.contractActionsContainerMultiInner { |
||||
width: 100%; |
||||
padding: 16px 8px 16px 14px; |
||||
border-radius: 3px; |
||||
margin-bottom: 8px; |
||||
} |
||||
.multiHeader { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-bottom: 8px; |
||||
text-align: left; |
||||
font-size: 10px; |
||||
font-weight: bold; |
||||
} |
||||
.contractActionsContainerMultiInner .multiTitle { |
||||
padding-left: 10px; |
||||
} |
||||
.contractProperty .multiTitle { |
||||
padding: 0; |
||||
line-height: 16px; |
||||
display: inline-block; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
cursor: default; |
||||
} |
||||
.contractProperty .contractActionsContainerMultiInner .multiArg label{ |
||||
text-align: right; |
||||
} |
||||
.multiHeader .methCaret { |
||||
float: right; |
||||
margin-right: 0; |
||||
} |
||||
.contractProperty.constant .multiTitle { |
||||
display: inline-block; |
||||
width: 90%; |
||||
/* font-size: 10px; */ |
||||
height: 25px; |
||||
padding-left: 20px; |
||||
font-weight: bold; |
||||
line-height: 25px; |
||||
cursor: default; |
||||
} |
||||
.multiArg { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: flex-end; |
||||
margin-top: 4px; |
||||
} |
||||
.multiArg input{ |
||||
padding: 5px; |
||||
} |
||||
.multiArg label { |
||||
width: auto; |
||||
padding: 0; |
||||
margin: 0 4px 0 0; |
||||
font-size: 10px; |
||||
line-height: 12px; |
||||
text-align: right; |
||||
word-break: initial; |
||||
} |
||||
.multiArg button { |
||||
max-width: 100px; |
||||
border-radius: 3px; |
||||
border-width: 1px; |
||||
width: inherit; |
||||
} |
||||
.multiHeader button { |
||||
display: inline-block; |
||||
width: 94%; |
||||
} |
||||
.hasArgs .multiArg input { |
||||
border-left: 1px solid #dddddd; |
||||
width: 67%; |
||||
} |
||||
.hasArgs input { |
||||
display: block; |
||||
height: 32px; |
||||
border: 1px solid #dddddd; |
||||
padding: .36em; |
||||
border-left: none; |
||||
padding: 8px 8px 8px 10px; |
||||
font-size: 10px !important; |
||||
} |
||||
.hasArgs button { |
||||
border-top-right-radius: 0; |
||||
border-bottom-right-radius: 0; |
||||
border-right: 0; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
font-size: 11px; |
||||
} |
||||
.hasArgs .contractActionsContainerMulti button { |
||||
border-radius: 3px; |
||||
} |
||||
.contractActionsContainerMultiInner .multiArg i { |
||||
padding-right: 10px; |
||||
} |
||||
.hideWarningsContainer { |
||||
display: flex; |
||||
align-items: center; |
||||
margin-left: 2% |
||||
} |
||||
` |
||||
|
||||
module.exports = css |
@ -1,16 +0,0 @@ |
||||
'use strict' |
||||
|
||||
var test = require('tape') |
||||
|
||||
var Compiler = require('@remix-project/remix-solidity').Compiler |
||||
|
||||
test('compiler.compile smoke', function (t) { |
||||
t.plan(1) |
||||
|
||||
var noop = function () {} |
||||
var fakeImport = function (url, cb) { cb('Not implemented') } |
||||
var compiler = new Compiler(fakeImport) |
||||
compiler.compileJSON = noop |
||||
compiler.compile({ 'test': '' }, 'test') |
||||
t.ok(compiler) |
||||
}) |
@ -1,52 +0,0 @@ |
||||
'use strict' |
||||
var modalDialogCustom |
||||
if (typeof window !== 'undefined') { |
||||
modalDialogCustom = require('../app/ui/modal-dialog-custom') |
||||
} |
||||
// ^ this class can be load in a non browser context when running node unit testing.
|
||||
// should not load UI in that case
|
||||
|
||||
// Allowing window to be overriden for testing
|
||||
function GistHandler (_window) { |
||||
if (_window !== undefined) { |
||||
modalDialogCustom = _window |
||||
} |
||||
|
||||
this.handleLoad = function (params, cb) { |
||||
if (!cb) cb = () => {} |
||||
var loadingFromGist = false |
||||
var gistId |
||||
if (params['gist'] === '') { |
||||
loadingFromGist = true |
||||
modalDialogCustom.prompt( |
||||
'Load a Gist', |
||||
'Enter the URL or ID of the Gist you would like to load.', |
||||
null, |
||||
target => { |
||||
if (target !== '') { |
||||
gistId = getGistId(target) |
||||
if (gistId) { |
||||
cb(gistId) |
||||
} |
||||
} |
||||
} |
||||
) |
||||
return loadingFromGist |
||||
} else { |
||||
gistId = params['gist'] |
||||
loadingFromGist = !!gistId |
||||
} |
||||
if (loadingFromGist) { |
||||
cb(gistId) |
||||
} |
||||
return loadingFromGist |
||||
} |
||||
|
||||
function getGistId (str) { |
||||
var idr = /[0-9A-Fa-f]{8,}/ |
||||
var match = idr.exec(str) |
||||
return match ? match[0] : null |
||||
} |
||||
} |
||||
|
||||
module.exports = GistHandler |
@ -1,5 +0,0 @@ |
||||
'use strict' |
||||
|
||||
require('./compiler-test') |
||||
require('./gist-handler-test') |
||||
require('./query-params-test') |
@ -1,23 +0,0 @@ |
||||
'use strict' |
||||
|
||||
var test = require('tape') |
||||
|
||||
var QueryParams = require('../src/lib/query-params') |
||||
|
||||
test('queryParams.get', function (t) { |
||||
t.plan(2) |
||||
|
||||
var fakeWindow = {location: {hash: '#wat=sup&foo=bar', search: ''}} |
||||
var params = new QueryParams(fakeWindow).get() |
||||
t.equal(params.wat, 'sup') |
||||
t.equal(params.foo, 'bar') |
||||
}) |
||||
|
||||
test('queryParams.update', function (t) { |
||||
t.plan(1) |
||||
|
||||
var fakeWindow = {location: {hash: '#wat=sup', search: ''}} |
||||
var qp = new QueryParams(fakeWindow) |
||||
qp.update({foo: 'bar'}) |
||||
t.equal(fakeWindow.location.hash, '#wat=sup&foo=bar') |
||||
}) |
@ -0,0 +1,138 @@ |
||||
/* global fetch */ |
||||
'use strict' |
||||
import { Plugin } from '@remixproject/engine' |
||||
|
||||
interface StringByString { |
||||
[key: string]: string; |
||||
} |
||||
|
||||
const profile = { |
||||
name: 'gistHandler', |
||||
methods: ['load'], |
||||
events: [], |
||||
version: '0.0.1' |
||||
} |
||||
|
||||
export class GistHandler extends Plugin { |
||||
constructor () { |
||||
super(profile) |
||||
} |
||||
|
||||
async handleLoad (gistId: String | null, cb: Function) { |
||||
if (!cb) cb = () => {} |
||||
|
||||
var loadingFromGist = false |
||||
if (!gistId) { |
||||
loadingFromGist = true |
||||
let value |
||||
try { |
||||
value = await (() => { |
||||
return new Promise((resolve, reject) => { |
||||
const modalContent = { |
||||
id: 'gisthandler', |
||||
title: 'Load a Gist', |
||||
message: 'Enter the ID of the Gist or URL you would like to load.', |
||||
modalType: 'prompt', |
||||
okLabel: 'OK', |
||||
cancelLabel: 'Cancel', |
||||
okFn: (value) => { |
||||
setTimeout(() => resolve(value), 0) |
||||
}, |
||||
cancelFn: () => { |
||||
setTimeout(() => reject(new Error('Canceled')), 0) |
||||
}, |
||||
hideFn: () => { |
||||
setTimeout(() => reject(new Error('Hide')), 0) |
||||
} |
||||
} |
||||
this.call('modal', 'modal', modalContent) |
||||
}) |
||||
})() |
||||
} catch (e) { |
||||
// the modal has been canceled
|
||||
return |
||||
} |
||||
|
||||
if (value !== '') { |
||||
gistId = getGistId(value) |
||||
if (gistId) { |
||||
cb(gistId) |
||||
} else { |
||||
const modalContent = { |
||||
id: 'gisthandler', |
||||
title: 'Gist load error', |
||||
message: 'Error while loading gist. Please provide a valid Gist ID or URL.' |
||||
} |
||||
this.call('modal', 'alert', modalContent) |
||||
} |
||||
} else { |
||||
const modalContent = { |
||||
id: 'gisthandlerEmpty', |
||||
title: 'Gist load error', |
||||
message: 'Error while loading gist. Id cannot be empty.' |
||||
} |
||||
this.call('modal', 'alert', modalContent) |
||||
} |
||||
return loadingFromGist |
||||
} else { |
||||
loadingFromGist = !!gistId |
||||
} |
||||
if (loadingFromGist) { |
||||
cb(gistId) |
||||
} |
||||
return loadingFromGist |
||||
} |
||||
|
||||
load (gistId: String | null) { |
||||
const self = this |
||||
return self.handleLoad(gistId, async (gistId: String | null) => { |
||||
let data: any |
||||
try { |
||||
data = await (await fetch(`https://api.github.com/gists/${gistId}`)).json() as any |
||||
if (!data.files) { |
||||
const modalContent = { |
||||
id: 'gisthandler', |
||||
title: 'Gist load error', |
||||
message: data.message, |
||||
modalType: 'alert', |
||||
okLabel: 'OK' |
||||
} |
||||
await this.call('modal', 'modal', modalContent) |
||||
return |
||||
} |
||||
} catch (e: any) { |
||||
const modalContent = { |
||||
id: 'gisthandler', |
||||
title: 'Gist load error', |
||||
message: e.message |
||||
|
||||
} |
||||
await this.call('modal', 'alert', modalContent) |
||||
return |
||||
} |
||||
|
||||
const obj: StringByString = {} |
||||
Object.keys(data.files).forEach((element) => { |
||||
const path = element.replace(/\.\.\./g, '/') |
||||
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element] |
||||
}) |
||||
this.call('fileManager', 'setBatchFiles', obj, 'workspace', true, async (errorSavingFiles: any) => { |
||||
if (errorSavingFiles) { |
||||
const modalContent = { |
||||
id: 'gisthandler', |
||||
title: 'Gist load error', |
||||
message: errorSavingFiles.message || errorSavingFiles |
||||
|
||||
} |
||||
this.call('modal', 'alert', modalContent) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
const getGistId = (str) => { |
||||
var idr = /[0-9A-Fa-f]{8,}/ |
||||
var match = idr.exec(str) |
||||
return match ? match[0] : null |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue