Merge pull request #4525 from ethereum/vypercompile-contextmenu

Vyper Contract compile FE
pull/4531/head
Joseph Izang 9 months ago committed by GitHub
commit 5988c71fd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 24
      apps/remix-ide-e2e/src/tests/vyper_api.test.ts
  2. 11
      apps/remix-ide/src/remixAppManager.js
  3. 28
      apps/vyper/src/app/app.tsx
  4. 111
      apps/vyper/src/app/components/CompilerButton.tsx
  5. 127
      apps/vyper/src/app/utils/compiler.tsx
  6. 27
      apps/vyper/src/app/utils/remix-client.tsx
  7. 2
      apps/vyper/src/profile.json
  8. 13
      libs/remix-ui/vyper-compile-details/src/lib/vyper-compile-details.tsx
  9. 1
      libs/remix-ui/vyper-compile-details/src/lib/vyperCompile.tsx

@ -41,8 +41,30 @@ module.exports = {
.openFile('examples/auctions/blind_auction.vy')
},
'Context menu click to compile blind_auction should succeed #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="treeViewLitreeViewItemexamples/auctions/blind_auction.vy"]')
.rightClick('*[data-id="treeViewLitreeViewItemexamples/auctions/blind_auction.vy"]')
.waitForElementPresent('[data-id="contextMenuItemvyper"]')
.click('[data-id="contextMenuItemvyper"]')
.clickLaunchIcon('vyper')
// @ts-ignore
.frame(0)
.waitForElementVisible({
selector:'[data-id="compilation-details"]',
timeout: 120000
})
.click('[data-id="compilation-details"]')
.frameParent()
.waitForElementVisible('[data-id="copy-abi"]')
.waitForElementVisible({
selector: "//*[@class='variable-value' and contains(.,'highestBidder')]",
locateStrategy: 'xpath',
})
},
'Compile blind_auction should success #group1': function (browser: NightwatchBrowser) {
browser.clickLaunchIcon('vyper')
browser
// @ts-ignore
.frame(0)
.click('[data-id="remote-compiler"]')

@ -331,6 +331,17 @@ export class RemixAppManager extends PluginManager {
sticky: true,
group: 7
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'vyper',
name: 'vyperCompileCustomAction',
label: 'Compile for Vyper',
type: [],
extension: ['.vy'],
path: [],
pattern: [],
sticky: true,
group: 7
})
if (Registry.getInstance().get('platform').api.isDesktop()) {
await this.call('filePanel', 'registerContextMenuItem', {
id: 'fs',

@ -28,7 +28,7 @@ interface OutputMap {
const App = () => {
const [contract, setContract] = useState<string>()
const [output, setOutput] = useState<any>({})
const [output, setOutput] = useState<any>(remixClient.compilerOutput)
const [state, setState] = useState<AppState>({
status: 'idle',
environment: 'remote',
@ -53,6 +53,30 @@ const App = () => {
start()
}, [])
useEffect(() => {
remixClient.eventEmitter.on('resetCompilerState', () => {
resetCompilerResultState()
})
return () => {
remixClient.eventEmitter.off('resetCompilerState', () => {
resetCompilerResultState()
})
}
}, [])
useEffect(() => {
remixClient.eventEmitter.on('setOutput', (payload) => {
setOutput(payload)
})
return () => {
remixClient.eventEmitter.off('setOutput', (payload) => {
setOutput(payload)
})
}
}, [])
/** Update the environment state value */
function setEnvironment(environment: 'local' | 'remote') {
setState({...state, environment})
@ -67,7 +91,7 @@ const App = () => {
}
function resetCompilerResultState() {
setOutput({})
setOutput(remixClient.compilerOutput)
}
return (

@ -1,7 +1,6 @@
import React, { Fragment, useState } from 'react'
import {isVyper, compile, toStandardOutput, isCompilationError, remixClient, normalizeContractPath} from '../utils'
import {isVyper, compile, toStandardOutput, isCompilationError, remixClient, normalizeContractPath, compileContract} from '../utils'
import Button from 'react-bootstrap/Button'
import _ from 'lodash'
interface Props {
compilerUrl: string
@ -21,112 +20,14 @@ function CompilerButton({contract, setOutput, compilerUrl, resetCompilerState}:
}
/** Compile a Contract */
async function compileContract() {
resetCompilerState()
setLoadingSpinnerState(true)
try {
// await remixClient.discardHighlight()
let _contract: any
try {
_contract = await remixClient.getContract()
} catch (e: any) {
setOutput('', {status: 'failed', message: e.message})
return
}
remixClient.changeStatus({
key: 'loading',
type: 'info',
title: 'Compiling'
})
let output
try {
output = await compile(compilerUrl, _contract)
} catch (e: any) {
remixClient.changeStatus({
key: 'failed',
type: 'error',
title: e.message
})
return
}
const compileReturnType = () => {
const t: any = toStandardOutput(contract, output)
const temp = _.merge(t['contracts'][contract])
const normal = normalizeContractPath(contract)[2]
const abi = temp[normal]['abi']
const evm = _.merge(temp[normal]['evm'])
const dpb = evm.deployedBytecode
const runtimeBytecode = evm.bytecode
const methodIdentifiers = evm.methodIdentifiers
const result = {
contractName: normal,
abi: abi,
bytecode: dpb,
runtimeBytecode: runtimeBytecode,
ir: '',
methodIdentifiers: methodIdentifiers
}
return result
}
// ERROR
if (isCompilationError(output)) {
const line = output.line
if (line) {
const lineColumnPos = {
start: {line: line - 1, column: 10},
end: {line: line - 1, column: 10}
}
// remixClient.highlight(lineColumnPos as any, _contract.name, '#e0b4b4')
} else {
const regex = output?.message?.match(/line ((\d+):(\d+))+/g)
const errors = output?.message?.split(/line ((\d+):(\d+))+/g) // extract error message
if (regex) {
let errorIndex = 0
regex.map((errorLocation) => {
const location = errorLocation?.replace('line ', '').split(':')
let message = errors[errorIndex]
errorIndex = errorIndex + 4
if (message && message?.split('\n\n').length > 0) {
try {
message = message?.split('\n\n')[message.split('\n\n').length - 1]
} catch (e) {}
}
if (location?.length > 0) {
const lineColumnPos = {
start: {line: parseInt(location[0]) - 1, column: 10},
end: {line: parseInt(location[0]) - 1, column: 10}
}
// remixClient.highlight(lineColumnPos as any, _contract.name, message)
}
})
}
}
throw new Error(output.message)
}
// SUCCESS
// remixClient.discardHighlight()
remixClient.changeStatus({
key: 'succeed',
type: 'success',
title: 'success'
})
const data = toStandardOutput(_contract.name, output)
remixClient.compilationFinish(_contract.name, _contract.content, data)
setOutput(_contract.name, compileReturnType())
} catch (err: any) {
remixClient.changeStatus({
key: 'failed',
type: 'error',
title: err.message
})
}
}
return (
<Fragment>
<button data-id="compile" onClick={compileContract} title={contract} className="btn btn-primary w-100 d-block btn-block text-break remixui_disabled mb-1 mt-3">
<button data-id="compile"
onClick={() => compileContract(contract, compilerUrl, setOutput)}
title={contract}
className="btn btn-primary w-100 d-block btn-block text-break remixui_disabled mb-1 mt-3"
>
<div className="d-flex align-items-center justify-content-center fa-1x">
<span className="fas fa-sync fa-pulse mr-1" />
<div className="text-truncate overflow-hidden text-nowrap">

@ -1,5 +1,8 @@
import { ABIDescription} from '@remixproject/plugin-api'
import axios from 'axios'
import { remixClient } from './remix-client'
import _ from 'lodash'
export interface Contract {
name: string
@ -167,6 +170,130 @@ export function toStandardOutput(fileName: string, compilationResult: any): any
}
}
}
export async function compileContract(contract: string, compilerUrl: string, setOutput?: any) {
remixClient.eventEmitter.emit('resetCompilerState', {})
try {
// await remixClient.discardHighlight()
let _contract: any
try {
_contract = await remixClient.getContract()
} catch (e: any) {
if (setOutput === null || setOutput === undefined) {
const compileResult = {
status: 'failed',
message: e.message
}
const compileResultKey = ''
remixClient.eventEmitter.emit('setOutput', { compileResultKey, compileResult })
} else {
setOutput('', {status: 'failed', message: e.message})
}
return
}
remixClient.changeStatus({
key: 'loading',
type: 'info',
title: 'Compiling'
})
let output
try {
output = await compile(compilerUrl, _contract)
} catch (e: any) {
remixClient.changeStatus({
key: 'failed',
type: 'error',
title: e.message
})
return
}
const compileReturnType = () => {
const t: any = toStandardOutput(contract, output)
const temp = _.merge(t['contracts'][contract])
const normal = normalizeContractPath(contract)[2]
const abi = temp[normal]['abi']
const evm = _.merge(temp[normal]['evm'])
const dpb = evm.deployedBytecode
const runtimeBytecode = evm.bytecode
const methodIdentifiers = evm.methodIdentifiers
const result = {
contractName: normal,
abi: abi,
bytecode: dpb,
runtimeBytecode: runtimeBytecode,
ir: '',
methodIdentifiers: methodIdentifiers
}
return result
}
// ERROR
if (isCompilationError(output)) {
const line = output.line
if (line) {
const lineColumnPos = {
start: {line: line - 1, column: 10},
end: {line: line - 1, column: 10}
}
// remixClient.highlight(lineColumnPos as any, _contract.name, '#e0b4b4')
} else {
const regex = output?.message?.match(/line ((\d+):(\d+))+/g)
const errors = output?.message?.split(/line ((\d+):(\d+))+/g) // extract error message
if (regex) {
let errorIndex = 0
regex.map((errorLocation) => {
const location = errorLocation?.replace('line ', '').split(':')
let message = errors[errorIndex]
errorIndex = errorIndex + 4
if (message && message?.split('\n\n').length > 0) {
try {
message = message?.split('\n\n')[message.split('\n\n').length - 1]
} catch (e) {}
}
if (location?.length > 0) {
const lineColumnPos = {
start: {line: parseInt(location[0]) - 1, column: 10},
end: {line: parseInt(location[0]) - 1, column: 10}
}
// remixClient.highlight(lineColumnPos as any, _contract.name, message)
}
})
}
}
throw new Error(output.message)
}
// SUCCESS
// remixClient.discardHighlight()
remixClient.changeStatus({
key: 'succeed',
type: 'success',
title: 'success'
})
const data = toStandardOutput(_contract.name, output)
remixClient.compilationFinish(_contract.name, _contract.content, data)
if (setOutput === null || setOutput === undefined) {
const contractName = _contract['name']
const compileResult = compileReturnType()
remixClient.eventEmitter.emit('setOutput', { contractName, compileResult })
} else {
setOutput(_contract.name, compileReturnType())
}
} catch (err: any) {
remixClient.changeStatus({
key: 'failed',
type: 'error',
title: err.message
})
}
}
export type StandardOutput = {
sources: {
[fileName: string]: {

@ -1,12 +1,23 @@
import {HighlightPosition, CompilationResult, RemixApi} from '@remixproject/plugin-api'
import {HighlightPosition, CompilationResult, RemixApi, customAction} from '@remixproject/plugin-api'
import {Api, Status} from '@remixproject/plugin-utils'
import {createClient} from '@remixproject/plugin-webview'
import {PluginClient} from '@remixproject/plugin'
import {Contract} from './compiler'
import {Contract, compileContract} from './compiler'
import {ExampleContract} from '../components/VyperResult'
import EventEmitter from 'events'
export type VyperComplierAddress = 'https://vyper2.remixproject.org/' | 'http://localhost:8000/'
export class RemixClient extends PluginClient {
private client = createClient<Api, Readonly<RemixApi>>(this)
compilerUrl: VyperComplierAddress = 'https://vyper2.remixproject.org/'
compilerOutput: any
eventEmitter = new EventEmitter()
constructor() {
super()
this.compilerOutput = {}
}
loaded() {
return this.client.onload()
@ -26,6 +37,18 @@ export class RemixClient extends PluginClient {
})
}
resetCompilerState() {
this.compilerOutput = {}
this.eventEmitter.emit('resetCompilerState', {})
}
async vyperCompileCustomAction(action: customAction) {
//read selected contract from file explorer and create contract type
const contract = await this.getContract()
//compile contract
await compileContract(contract.name, this.compilerUrl)
}
/** Load Ballot contract example into the file manager */
async loadContract({name, address}: ExampleContract) {
try {

@ -1,7 +1,7 @@
{
"name": "vyper",
"displayName": "Vyper Plugin",
"methods": ["getCompilationResult", "compile"],
"methods": ["getCompilationResult", "compile", "vyperCompileCustomAction"],
"url": "https://ipfs-cluster.ethdevops.io/ipfs/QmbmPzUg7ghTKcF2eo64zm1k1LKdibYfqYmiqXkHKXks8r",
"documentation": "https://remix-ide.readthedocs.io/en/latest/plugin_list.html",
"description": "Compile vyper contracts",

@ -10,12 +10,13 @@ interface RemixUiVyperCompileDetailsProps {
}
export function RemixUiVyperCompileDetails({ payload, theme, themeStyle }: RemixUiVyperCompileDetailsProps) {
const dpayload = Object.values(payload) as any ?? {}
const bcode = dpayload[0].bytecode ? dpayload[0].bytecode.object : ''
const runtimeBcode = dpayload[0].runtimeBytecode ? dpayload[0].runtimeBytecode.object : ''
const ir = dpayload[0].ir
const methodIdentifiers= dpayload[0].methodIdentifiers
const abi= dpayload[0].abi
const compileResult = payload['compileResult'] ?? {}
console.log('compileResult', compileResult, payload)
const bcode = compileResult.bytecode ? compileResult.bytecode.object : ''
const runtimeBcode = compileResult.runtimeBytecode ? compileResult.runtimeBytecode.object : ''
const ir = compileResult.ir
const methodIdentifiers= compileResult.methodIdentifiers
const abi= compileResult.abi
return (
<>
<VyperCompile

@ -25,7 +25,6 @@ export interface VyperCompileProps {
}
export default function VyperCompile({result, theme, themeStyle}: VyperCompileProps) {
const [active, setActive] = useState<keyof VyperCompilationResult>('abi')
const tabContent = [
{

Loading…
Cancel
Save