commit
77209336e3
@ -0,0 +1,4 @@ |
||||
{ |
||||
"presets": ["@nrwl/react/babel"], |
||||
"plugins": [] |
||||
} |
@ -0,0 +1,18 @@ |
||||
{ |
||||
"env": { |
||||
"browser": true, |
||||
"es6": true |
||||
}, |
||||
"extends": "../../../.eslintrc", |
||||
"globals": { |
||||
"Atomics": "readonly", |
||||
"SharedArrayBuffer": "readonly" |
||||
}, |
||||
"parserOptions": { |
||||
"ecmaVersion": 11, |
||||
"sourceType": "module" |
||||
}, |
||||
"rules": { |
||||
"standard/no-callback-literal": "off" |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
# remix-ui-publish-to-storage |
||||
|
||||
This library was generated with [Nx](https://nx.dev). |
||||
|
||||
## Running unit tests |
||||
|
||||
Run `nx test remix-ui-publish-to-storage` to execute the unit tests via [Jest](https://jestjs.io). |
@ -0,0 +1 @@ |
||||
export * from './lib/publish-to-storage' |
@ -0,0 +1,110 @@ |
||||
import React, { useEffect, useState } from 'react' // eslint-disable-line
|
||||
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
|
||||
import { RemixUiPublishToStorageProps } from './types' // eslint-disable-line
|
||||
import { publishToIPFS } from './publishToIPFS' |
||||
import { publishToSwarm } from './publishOnSwarm' |
||||
|
||||
export const PublishToStorage = (props: RemixUiPublishToStorageProps) => { |
||||
const { storage, fileProvider, fileManager, contract, resetStorage } = props |
||||
const [state, setState] = useState({ |
||||
modal: { |
||||
title: '', |
||||
message: null, |
||||
hide: true, |
||||
okLabel: '', |
||||
okFn: null, |
||||
cancelLabel: '', |
||||
cancelFn: null |
||||
} |
||||
}) |
||||
|
||||
useEffect(() => { |
||||
const storageService = async () => { |
||||
if ((contract.metadata === undefined || contract.metadata.length === 0)) { |
||||
modal('Publish To Storage', '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') { |
||||
try { |
||||
const result = await publishToSwarm(contract, fileManager) |
||||
|
||||
modal(`Published ${contract.name}'s Metadata`, publishMessage(result.uploaded)) |
||||
// triggered each time there's a new verified publish (means hash correspond)
|
||||
fileProvider.addExternal('swarm/' + result.item.hash, result.item.content) |
||||
} catch (err) { |
||||
let parseError = err |
||||
try { |
||||
parseError = JSON.stringify(err) |
||||
} catch (e) {} |
||||
modal('Swarm Publish Failed', publishMessageFailed(storage, parseError)) |
||||
} |
||||
} else { |
||||
try { |
||||
const result = await publishToIPFS(contract, fileManager) |
||||
|
||||
modal(`Published ${contract.name}'s Metadata`, publishMessage(result.uploaded)) |
||||
// triggered each time there's a new verified publish (means hash correspond)
|
||||
fileProvider.addExternal('ipfs/' + result.item.hash, result.item.content) |
||||
} catch (err) { |
||||
modal('IPFS Publish Failed', publishMessageFailed(storage, err)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (storage) { |
||||
storageService() |
||||
} |
||||
}, [storage]) |
||||
|
||||
const publishMessage = (uploaded) => ( |
||||
<span> Metadata of "{contract.name.toLowerCase()}" was published successfully. <br /> |
||||
<pre> |
||||
<div> |
||||
{ uploaded.map((value, index) => <div key={index}><b>{ value.filename }</b> : <pre>{ value.output.url }</pre></div>) } |
||||
</div> |
||||
</pre> |
||||
</span> |
||||
) |
||||
|
||||
const publishMessageFailed = (storage, err) => ( |
||||
<span>Failed to publish metadata file to { storage }, please check the { storage } gateways is available. <br /> |
||||
{err} |
||||
</span> |
||||
) |
||||
|
||||
const handleHideModal = () => { |
||||
setState(prevState => { |
||||
return { ...prevState, modal: { ...prevState.modal, hide: true, message: null } } |
||||
}) |
||||
resetStorage() |
||||
} |
||||
|
||||
const modal = async (title: string, message: string | JSX.Element) => { |
||||
await setState(prevState => { |
||||
return { |
||||
...prevState, |
||||
modal: { |
||||
...prevState.modal, |
||||
hide: false, |
||||
message, |
||||
title |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
return ( |
||||
<ModalDialog |
||||
id='publishToStorage' |
||||
title={ state.modal.title } |
||||
message={ state.modal.message } |
||||
hide={ state.modal.hide } |
||||
okLabel='OK' |
||||
okFn={() => {}} |
||||
handleHide={ handleHideModal }> |
||||
{ (typeof state.modal.message !== 'string') && state.modal.message } |
||||
</ModalDialog> |
||||
) |
||||
} |
||||
|
||||
export default PublishToStorage |
@ -0,0 +1,109 @@ |
||||
import swarm from 'swarmgw' |
||||
|
||||
const swarmgw = swarm() |
||||
|
||||
export const publishToSwarm = async (contract, fileManager) => { |
||||
// gather list of files to publish
|
||||
const sources = [] |
||||
let metadata |
||||
const item = { content: null, hash: null } |
||||
const uploaded = [] |
||||
|
||||
try { |
||||
metadata = JSON.parse(contract.metadata) |
||||
} catch (e) { |
||||
throw new Error(e) |
||||
} |
||||
|
||||
if (metadata === undefined) { |
||||
throw new Error('No metadata') |
||||
} |
||||
|
||||
await Promise.all(Object.keys(metadata.sources).map(fileName => { |
||||
// 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) { |
||||
throw new Error('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 |
||||
}) |
||||
} |
||||
}) |
||||
})) |
||||
// publish the list of sources in order, fail if any failed
|
||||
|
||||
await Promise.all(sources.map(async (item) => { |
||||
try { |
||||
const result = await swarmVerifiedPublish(item.content, item.hash) |
||||
|
||||
try { |
||||
item.hash = result.url.match('bzz-raw://(.+)')[1] |
||||
} catch (e) { |
||||
item.hash = '<Metadata inconsistency> - ' + item.fileName |
||||
} |
||||
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 |
||||
} catch (error) { |
||||
throw new Error(error) |
||||
} |
||||
})) |
||||
|
||||
const metadataContent = JSON.stringify(metadata) |
||||
try { |
||||
const result = await swarmVerifiedPublish(metadataContent, '') |
||||
|
||||
try { |
||||
contract.metadataHash = result.url.match('bzz-raw://(.+)')[1] |
||||
} catch (e) { |
||||
contract.metadataHash = '<Metadata inconsistency> - metadata.json' |
||||
} |
||||
item.content = metadataContent |
||||
item.hash = contract.metadataHash |
||||
uploaded.push({ |
||||
content: contract.metadata, |
||||
hash: contract.metadataHash, |
||||
filename: 'metadata.json', |
||||
output: result |
||||
}) |
||||
} catch (error) { |
||||
throw new Error(error) |
||||
} |
||||
|
||||
return { uploaded, item } |
||||
} |
||||
|
||||
const swarmVerifiedPublish = async (content, expectedHash): Promise<Record<string, any>> => { |
||||
return new Promise((resolve, reject) => { |
||||
swarmgw.put(content, function (err, ret) { |
||||
if (err) { |
||||
reject(err) |
||||
} else if (expectedHash && ret !== expectedHash) { |
||||
resolve({ message: 'hash mismatch between solidity bytecode and uploaded content.', url: 'bzz-raw://' + ret, hash: ret }) |
||||
} else { |
||||
resolve({ message: 'ok', url: 'bzz-raw://' + ret, hash: ret }) |
||||
} |
||||
}) |
||||
}) |
||||
} |
@ -0,0 +1,117 @@ |
||||
import IpfsClient from 'ipfs-mini' |
||||
|
||||
const ipfsNodes = [ |
||||
new IpfsClient({ host: 'ipfs.komputing.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' }) |
||||
] |
||||
|
||||
export const publishToIPFS = async (contract, fileManager) => { |
||||
// gather list of files to publish
|
||||
const sources = [] |
||||
let metadata |
||||
const item = { content: null, hash: null } |
||||
const uploaded = [] |
||||
|
||||
try { |
||||
metadata = JSON.parse(contract.metadata) |
||||
} catch (e) { |
||||
throw new Error(e) |
||||
} |
||||
|
||||
if (metadata === undefined) { |
||||
throw new Error('No metadata') |
||||
} |
||||
|
||||
await Promise.all(Object.keys(metadata.sources).map(fileName => { |
||||
// 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) { |
||||
throw new Error('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 |
||||
}) |
||||
} |
||||
}) |
||||
})) |
||||
// publish the list of sources in order, fail if any failed
|
||||
await Promise.all(sources.map(async (item) => { |
||||
try { |
||||
const result = await ipfsVerifiedPublish(item.content, item.hash) |
||||
|
||||
try { |
||||
item.hash = result.url.match('dweb:/ipfs/(.+)')[1] |
||||
} catch (e) { |
||||
item.hash = '<Metadata inconsistency> - ' + item.fileName |
||||
} |
||||
item.output = result |
||||
uploaded.push(item) |
||||
} catch (error) { |
||||
throw new Error(error) |
||||
} |
||||
})) |
||||
const metadataContent = JSON.stringify(metadata) |
||||
|
||||
try { |
||||
const result = await ipfsVerifiedPublish(metadataContent, '') |
||||
|
||||
try { |
||||
contract.metadataHash = result.url.match('dweb:/ipfs/(.+)')[1] |
||||
} catch (e) { |
||||
contract.metadataHash = '<Metadata inconsistency> - metadata.json' |
||||
} |
||||
item.content = metadataContent |
||||
item.hash = contract.metadataHash |
||||
uploaded.push({ |
||||
content: contract.metadata, |
||||
hash: contract.metadataHash, |
||||
filename: 'metadata.json', |
||||
output: result |
||||
}) |
||||
} catch (error) { |
||||
throw new Error(error) |
||||
} |
||||
|
||||
return { uploaded, item } |
||||
} |
||||
|
||||
const ipfsVerifiedPublish = async (content, expectedHash) => { |
||||
try { |
||||
const results = await severalGatewaysPush(content) |
||||
|
||||
if (expectedHash && results !== expectedHash) { |
||||
return { message: 'hash mismatch between solidity bytecode and uploaded content.', url: 'dweb:/ipfs/' + results, hash: results } |
||||
} else { |
||||
return { message: 'ok', url: 'dweb:/ipfs/' + results, hash: results } |
||||
} |
||||
} catch (error) { |
||||
throw new Error(error) |
||||
} |
||||
} |
||||
|
||||
const 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)) |
||||
} |
@ -0,0 +1,7 @@ |
||||
export interface RemixUiPublishToStorageProps { |
||||
storage: string, |
||||
fileProvider: any, |
||||
fileManager: any, |
||||
contract: any, |
||||
resetStorage: () => void |
||||
} |
@ -0,0 +1,16 @@ |
||||
{ |
||||
"extends": "../../../tsconfig.json", |
||||
"compilerOptions": { |
||||
"jsx": "react", |
||||
"allowJs": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true |
||||
}, |
||||
"files": [], |
||||
"include": [], |
||||
"references": [ |
||||
{ |
||||
"path": "./tsconfig.lib.json" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,13 @@ |
||||
{ |
||||
"extends": "./tsconfig.json", |
||||
"compilerOptions": { |
||||
"outDir": "../../../dist/out-tsc", |
||||
"types": ["node"] |
||||
}, |
||||
"files": [ |
||||
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", |
||||
"../../../node_modules/@nrwl/react/typings/image.d.ts" |
||||
], |
||||
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"], |
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] |
||||
} |
@ -0,0 +1,4 @@ |
||||
{ |
||||
"presets": ["@nrwl/react/babel"], |
||||
"plugins": [] |
||||
} |
@ -0,0 +1,19 @@ |
||||
{ |
||||
"env": { |
||||
"browser": true, |
||||
"es6": true |
||||
}, |
||||
"extends": "../../../.eslintrc", |
||||
"globals": { |
||||
"Atomics": "readonly", |
||||
"SharedArrayBuffer": "readonly" |
||||
}, |
||||
"parserOptions": { |
||||
"ecmaVersion": 11, |
||||
"sourceType": "module" |
||||
}, |
||||
"rules": { |
||||
"no-unused-vars": "off", |
||||
"@typescript-eslint/no-unused-vars": "error" |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
# remix-ui-renderer |
||||
|
||||
This library was generated with [Nx](https://nx.dev). |
||||
|
||||
## Running unit tests |
||||
|
||||
Run `nx test remix-ui-renderer` to execute the unit tests via [Jest](https://jestjs.io). |
@ -0,0 +1 @@ |
||||
export * from './lib/renderer' |
@ -0,0 +1,47 @@ |
||||
.remixui_sol.success, |
||||
.remixui_sol.error, |
||||
.remixui_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; |
||||
} |
||||
|
||||
.remixui_sol.success pre, |
||||
.remixui_sol.error pre, |
||||
.remixui_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; |
||||
} |
||||
|
||||
.remixui_sol.success .close, |
||||
.remixui_sol.error .close, |
||||
.remixui_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; |
||||
} |
||||
|
||||
.remixui_sol.error { |
||||
} |
||||
|
||||
.remixui_sol.warning { |
||||
} |
||||
|
||||
.remixui_sol.success { |
||||
/* background-color: // styles.rightPanel.message_Success_BackgroundColor; */ |
||||
} |
@ -0,0 +1,127 @@ |
||||
import React, { useEffect, useState } from 'react' //eslint-disable-line
|
||||
import './renderer.css' |
||||
interface RendererProps { |
||||
message: any; |
||||
opt?: any, |
||||
plugin: any, |
||||
editor: any, |
||||
config: any, |
||||
fileManager: any |
||||
} |
||||
|
||||
export const Renderer = ({ message, opt = {}, editor, config, fileManager, plugin }: RendererProps) => { |
||||
const [messageText, setMessageText] = useState(null) |
||||
const [editorOptions, setEditorOptions] = useState({ |
||||
useSpan: false, |
||||
type: '', |
||||
errFile: '' |
||||
}) |
||||
const [classList] = useState(opt.type === 'error' ? 'alert alert-danger' : 'alert alert-warning') |
||||
const [close, setClose] = useState(false) |
||||
|
||||
useEffect(() => { |
||||
if (!message) return |
||||
let text |
||||
|
||||
if (typeof message === 'string') { |
||||
text = message |
||||
} 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 positionDetails = getPositionDetails(text) |
||||
const options = opt |
||||
|
||||
if (!positionDetails.errFile || (opt.errorType && opt.errorType === positionDetails.errFile)) { |
||||
// Updated error reported includes '-->' before file details
|
||||
const errorDetails = text.split('-->') |
||||
// errorDetails[1] will have file details
|
||||
if (errorDetails.length > 1) positionDetails = getPositionDetails(errorDetails[1]) |
||||
} |
||||
options.errLine = positionDetails.errLine |
||||
options.errCol = positionDetails.errCol |
||||
options.errFile = positionDetails.errFile.trim() |
||||
|
||||
if (!opt.noAnnotations && opt.errFile) { |
||||
addAnnotation(opt.errFile, { |
||||
row: opt.errLine, |
||||
column: opt.errCol, |
||||
text: text, |
||||
type: opt.type |
||||
}) |
||||
} |
||||
|
||||
setMessageText(text) |
||||
setEditorOptions(options) |
||||
setClose(false) |
||||
}, [message]) |
||||
|
||||
const getPositionDetails = (msg: any) => { |
||||
const result = { } as Record<string, number | string> |
||||
|
||||
// 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 pos = msg.match(/^(.*?):([0-9]*?):([0-9]*?)?/) |
||||
result.errLine = pos ? parseInt(pos[2]) - 1 : -1 |
||||
result.errCol = pos ? parseInt(pos[3]) : -1 |
||||
|
||||
// extract file
|
||||
pos = msg.match(/^(https:.*?|http:.*?|.*?):/) |
||||
result.errFile = pos ? pos[1] : '' |
||||
return result |
||||
} |
||||
|
||||
const addAnnotation = (file, error) => { |
||||
if (file === config.get('currentFile')) { |
||||
plugin.call('editor', 'addAnnotation', error, file) |
||||
} |
||||
} |
||||
|
||||
const handleErrorClick = (opt) => { |
||||
if (opt.click) { |
||||
opt.click(message) |
||||
} else if (opt.errFile !== undefined && opt.errLine !== undefined && opt.errCol !== undefined) { |
||||
_errorClick(opt.errFile, opt.errLine, opt.errCol) |
||||
} |
||||
} |
||||
|
||||
const handleClose = () => { |
||||
setClose(true) |
||||
} |
||||
|
||||
const _errorClick = (errFile, errLine, errCol) => { |
||||
if (errFile !== config.get('currentFile')) { |
||||
// TODO: refactor with this._components.contextView.jumpTo
|
||||
const provider = fileManager.fileProviderOf(errFile) |
||||
if (provider) { |
||||
provider.exists(errFile).then(() => { |
||||
fileManager.open(errFile) |
||||
editor.gotoLine(errLine, errCol) |
||||
}).catch(error => { |
||||
if (error) return console.log(error) |
||||
}) |
||||
} |
||||
} else { |
||||
editor.gotoLine(errLine, errCol) |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
{ |
||||
messageText && !close && ( |
||||
<div className={`sol ${editorOptions.type} ${classList}`} data-id={editorOptions.errFile} onClick={() => handleErrorClick(editorOptions)}> |
||||
{ editorOptions.useSpan ? <span> { messageText } </span> : <pre><span>{ messageText }</span></pre> } |
||||
<div className="close" data-id="renderer" onClick={handleClose}> |
||||
<i className="fas fa-times"></i> |
||||
</div> |
||||
</div>) |
||||
} |
||||
</> |
||||
) |
||||
} |
@ -0,0 +1,16 @@ |
||||
{ |
||||
"extends": "../../../tsconfig.json", |
||||
"compilerOptions": { |
||||
"jsx": "react", |
||||
"allowJs": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true |
||||
}, |
||||
"files": [], |
||||
"include": [], |
||||
"references": [ |
||||
{ |
||||
"path": "./tsconfig.lib.json" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,13 @@ |
||||
{ |
||||
"extends": "./tsconfig.json", |
||||
"compilerOptions": { |
||||
"outDir": "../../../dist/out-tsc", |
||||
"types": ["node"] |
||||
}, |
||||
"files": [ |
||||
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", |
||||
"../../../node_modules/@nrwl/react/typings/image.d.ts" |
||||
], |
||||
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"], |
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] |
||||
} |
@ -0,0 +1,4 @@ |
||||
{ |
||||
"presets": ["@nrwl/react/babel"], |
||||
"plugins": [] |
||||
} |
@ -0,0 +1,19 @@ |
||||
{ |
||||
"env": { |
||||
"browser": true, |
||||
"es6": true |
||||
}, |
||||
"extends": "../../../.eslintrc", |
||||
"globals": { |
||||
"Atomics": "readonly", |
||||
"SharedArrayBuffer": "readonly" |
||||
}, |
||||
"parserOptions": { |
||||
"ecmaVersion": 11, |
||||
"sourceType": "module" |
||||
}, |
||||
"rules": { |
||||
"no-unused-vars": "off", |
||||
"@typescript-eslint/no-unused-vars": "error" |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
# remix-ui-solidity-compiler |
||||
|
||||
This library was generated with [Nx](https://nx.dev). |
||||
|
||||
## Running unit tests |
||||
|
||||
Run `nx test remix-ui-solidity-compiler` to execute the unit tests via [Jest](https://jestjs.io). |
@ -0,0 +1,2 @@ |
||||
export * from './lib/solidity-compiler' |
||||
export * from './lib/logic' |
@ -0,0 +1,57 @@ |
||||
import React from 'react' |
||||
|
||||
export const setEditorMode = (mode: string) => { |
||||
return { |
||||
type: 'SET_EDITOR_MODE', |
||||
payload: mode |
||||
} |
||||
} |
||||
|
||||
export const resetEditorMode = () => (dispatch: React.Dispatch<any>) => { |
||||
dispatch({ |
||||
type: 'RESET_EDITOR_MODE' |
||||
}) |
||||
} |
||||
|
||||
export const setCompilerMode = (mode: string, ...args) => { |
||||
return { |
||||
type: 'SET_COMPILER_MODE', |
||||
payload: { mode, args } |
||||
} |
||||
} |
||||
|
||||
export const resetCompilerMode = () => (dispatch: React.Dispatch<any>) => { |
||||
dispatch({ |
||||
type: 'RESET_COMPILER_MODE' |
||||
}) |
||||
} |
||||
|
||||
export const listenToEvents = (editor, compileTabLogic) => (dispatch: React.Dispatch<any>) => { |
||||
editor.event.register('sessionSwitched', () => { |
||||
dispatch(setEditorMode('sessionSwitched')) |
||||
}) |
||||
|
||||
compileTabLogic.event.on('startingCompilation', () => { |
||||
dispatch(setCompilerMode('startingCompilation')) |
||||
}) |
||||
|
||||
compileTabLogic.compiler.event.register('compilationDuration', (speed) => { |
||||
dispatch(setCompilerMode('compilationDuration', speed)) |
||||
}) |
||||
|
||||
editor.event.register('contentChanged', () => { |
||||
dispatch(setEditorMode('contentChanged')) |
||||
}) |
||||
|
||||
compileTabLogic.compiler.event.register('loadingCompiler', () => { |
||||
dispatch(setCompilerMode('compilationDuration')) |
||||
}) |
||||
|
||||
compileTabLogic.compiler.event.register('compilerLoaded', () => { |
||||
dispatch(setCompilerMode('compilerLoaded')) |
||||
}) |
||||
|
||||
compileTabLogic.compiler.event.register('compilationFinished', (success, data, source) => { |
||||
dispatch(setCompilerMode('compilationFinished', success, data, source)) |
||||
}) |
||||
} |
@ -0,0 +1,582 @@ |
||||
import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint-disable-line
|
||||
import semver from 'semver' |
||||
import { CompilerContainerProps, ConfigurationSettings } from './types' |
||||
import * as helper from '../../../../../apps/remix-ide/src/lib/helper' |
||||
import { canUseWorker, baseURLBin, baseURLWasm, urlFromVersion, pathToURL, promisedMiniXhr } from '@remix-project/remix-solidity' // @ts-ignore
|
||||
import { compilerReducer, compilerInitialState } from './reducers/compiler' |
||||
import { resetEditorMode, listenToEvents } from './actions/compiler' |
||||
|
||||
import './css/style.css' |
||||
|
||||
export const CompilerContainer = (props: CompilerContainerProps) => { |
||||
const { editor, config, queryParams, compileTabLogic, tooltip, modal, compiledFileName, setHardHatCompilation, updateCurrentVersion, isHardHatProject, configurationSettings } = props // eslint-disable-line
|
||||
const [state, setState] = useState({ |
||||
hideWarnings: false, |
||||
autoCompile: false, |
||||
optimise: false, |
||||
compileTimeout: null, |
||||
timeout: 300, |
||||
allversions: [], |
||||
customVersions: [], |
||||
selectedVersion: null, |
||||
defaultVersion: 'soljson-v0.8.4+commit.c7e474f2.js', // this default version is defined: in makeMockCompiler (for browser test)
|
||||
selectedLanguage: '', |
||||
runs: '', |
||||
compiledFileName: '', |
||||
includeNightlies: false, |
||||
language: '', |
||||
evmVersion: '' |
||||
}) |
||||
const compileIcon = useRef(null) |
||||
const warningIcon = useRef(null) |
||||
const promptMessageInput = useRef(null) |
||||
const [hhCompilation, sethhCompilation] = useState(false) |
||||
const [compilerContainer, dispatch] = useReducer(compilerReducer, compilerInitialState) |
||||
|
||||
useEffect(() => { |
||||
fetchAllVersion((allversions, selectedVersion, isURL) => { |
||||
setState(prevState => { |
||||
return { ...prevState, allversions } |
||||
}) |
||||
if (isURL) _updateVersionSelector(state.defaultVersion, selectedVersion) |
||||
else { |
||||
setState(prevState => { |
||||
return { ...prevState, selectedVersion } |
||||
}) |
||||
updateCurrentVersion(selectedVersion) |
||||
_updateVersionSelector(selectedVersion) |
||||
} |
||||
}) |
||||
const currentFileName = config.get('currentFile') |
||||
|
||||
currentFile(currentFileName) |
||||
listenToEvents(editor, compileTabLogic)(dispatch) |
||||
}, []) |
||||
|
||||
useEffect(() => { |
||||
if (compileTabLogic && compileTabLogic.compiler) { |
||||
setState(prevState => { |
||||
const params = queryParams.get() |
||||
const optimize = params.optimize === 'false' ? false : params.optimize === 'true' ? true : null |
||||
const runs = params.runs |
||||
const evmVersion = params.evmVersion |
||||
|
||||
return { |
||||
...prevState, |
||||
hideWarnings: config.get('hideWarnings') || false, |
||||
autoCompile: config.get('autoCompile') || false, |
||||
includeNightlies: config.get('includeNightlies') || false, |
||||
optimise: (optimize !== null) && (optimize !== undefined) ? optimize : config.get('optimise') || false, |
||||
runs: (runs !== null) && (runs !== 'null') && (runs !== undefined) && (runs !== 'undefined') ? runs : 200, |
||||
evmVersion: (evmVersion !== null) && (evmVersion !== 'null') && (evmVersion !== undefined) && (evmVersion !== 'undefined') ? evmVersion : 'default' |
||||
} |
||||
}) |
||||
} |
||||
}, [compileTabLogic]) |
||||
|
||||
useEffect(() => { |
||||
setState(prevState => { |
||||
return { ...prevState, compiledFileName } |
||||
}) |
||||
}, [compiledFileName]) |
||||
|
||||
useEffect(() => { |
||||
if (compilerContainer.compiler.mode) { |
||||
switch (compilerContainer.compiler.mode) { |
||||
case 'startingCompilation': |
||||
startingCompilation() |
||||
break |
||||
case 'compilationDuration': |
||||
compilationDuration(compilerContainer.compiler.args[0]) |
||||
break |
||||
case 'loadingCompiler': |
||||
loadingCompiler() |
||||
break |
||||
case 'compilerLoaded': |
||||
compilerLoaded() |
||||
break |
||||
case 'compilationFinished': |
||||
compilationFinished() |
||||
break |
||||
} |
||||
} |
||||
}, [compilerContainer.compiler.mode]) |
||||
|
||||
useEffect(() => { |
||||
if (compilerContainer.editor.mode) { |
||||
switch (compilerContainer.editor.mode) { |
||||
case 'sessionSwitched': |
||||
sessionSwitched() |
||||
resetEditorMode()(dispatch) |
||||
break |
||||
case 'contentChanged': |
||||
contentChanged() |
||||
resetEditorMode()(dispatch) |
||||
break |
||||
} |
||||
} |
||||
}, [compilerContainer.editor.mode]) |
||||
|
||||
useEffect(() => { |
||||
if (configurationSettings) { |
||||
setConfiguration(configurationSettings) |
||||
} |
||||
}, [configurationSettings]) |
||||
|
||||
// fetching both normal and wasm builds and creating a [version, baseUrl] map
|
||||
const fetchAllVersion = async (callback) => { |
||||
let selectedVersion, allVersionsWasm, isURL |
||||
let allVersions = [{ path: 'builtin', longVersion: 'latest local version - 0.7.4' }] |
||||
// fetch normal builds
|
||||
const binRes: any = await promisedMiniXhr(`${baseURLBin}/list.json`) |
||||
// fetch wasm builds
|
||||
const wasmRes: any = await promisedMiniXhr(`${baseURLWasm}/list.json`) |
||||
if (binRes.event.type === 'error' && wasmRes.event.type === 'error') { |
||||
selectedVersion = 'builtin' |
||||
return callback(allVersions, selectedVersion) |
||||
} |
||||
try { |
||||
const versions = JSON.parse(binRes.json).builds.slice().reverse() |
||||
|
||||
allVersions = [...allVersions, ...versions] |
||||
selectedVersion = state.defaultVersion |
||||
if (queryParams.get().version) selectedVersion = queryParams.get().version |
||||
// Check if version is a URL and corresponding filename starts with 'soljson'
|
||||
if (selectedVersion.startsWith('https://')) { |
||||
const urlArr = selectedVersion.split('/') |
||||
|
||||
if (urlArr[urlArr.length - 1].startsWith('soljson')) isURL = true |
||||
} |
||||
if (wasmRes.event.type !== 'error') { |
||||
allVersionsWasm = JSON.parse(wasmRes.json).builds.slice().reverse() |
||||
} |
||||
} catch (e) { |
||||
tooltip('Cannot load compiler version list. It might have been blocked by an advertisement blocker. Please try deactivating any of them from this page and reload. Error: ' + e) |
||||
} |
||||
// replace in allVersions those compiler builds which exist in allVersionsWasm with new once
|
||||
if (allVersionsWasm && allVersions) { |
||||
allVersions.forEach((compiler, index) => { |
||||
const wasmIndex = allVersionsWasm.findIndex(wasmCompiler => { return wasmCompiler.longVersion === compiler.longVersion }) |
||||
if (wasmIndex !== -1) { |
||||
allVersions[index] = allVersionsWasm[wasmIndex] |
||||
pathToURL[compiler.path] = baseURLWasm |
||||
} else { |
||||
pathToURL[compiler.path] = baseURLBin |
||||
} |
||||
}) |
||||
} |
||||
callback(allVersions, selectedVersion, isURL) |
||||
} |
||||
|
||||
/** |
||||
* Update the compilation button with the name of the current file |
||||
*/ |
||||
const currentFile = (name = '') => { |
||||
if (name && name !== '') { |
||||
_setCompilerVersionFromPragma(name) |
||||
} |
||||
const compiledFileName = name.split('/').pop() |
||||
|
||||
setState(prevState => { |
||||
return { ...prevState, compiledFileName } |
||||
}) |
||||
} |
||||
|
||||
// Load solc compiler version according to pragma in contract file
|
||||
const _setCompilerVersionFromPragma = (filename: string) => { |
||||
if (!state.allversions) return |
||||
compileTabLogic.fileManager.readFile(filename).then(data => { |
||||
const pragmaArr = data.match(/(pragma solidity (.+?);)/g) |
||||
if (pragmaArr && pragmaArr.length === 1) { |
||||
const pragmaStr = pragmaArr[0].replace('pragma solidity', '').trim() |
||||
const pragma = pragmaStr.substring(0, pragmaStr.length - 1) |
||||
const releasedVersions = state.allversions.filter(obj => !obj.prerelease).map(obj => obj.version) |
||||
const allVersions = state.allversions.map(obj => _retrieveVersion(obj.version)) |
||||
const currentCompilerName = _retrieveVersion(state.selectedVersion) |
||||
// contains only numbers part, for example '0.4.22'
|
||||
const pureVersion = _retrieveVersion() |
||||
// is nightly build newer than the last release
|
||||
const isNewestNightly = currentCompilerName.includes('nightly') && semver.gt(pureVersion, releasedVersions[0]) |
||||
// checking if the selected version is in the pragma range
|
||||
const isInRange = semver.satisfies(pureVersion, pragma) |
||||
// checking if the selected version is from official compilers list(excluding custom versions) and in range or greater
|
||||
const isOfficial = allVersions.includes(currentCompilerName) |
||||
if (isOfficial && (!isInRange && !isNewestNightly)) { |
||||
const compilerToLoad = semver.maxSatisfying(releasedVersions, pragma) |
||||
const compilerPath = state.allversions.filter(obj => !obj.prerelease && obj.version === compilerToLoad)[0].path |
||||
if (state.selectedVersion !== compilerPath) { |
||||
setState((prevState) => { |
||||
return { ...prevState, selectedVersion: compilerPath } |
||||
}) |
||||
_updateVersionSelector(compilerPath) |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
const isSolFileSelected = (currentFile = '') => { |
||||
if (!currentFile) currentFile = config.get('currentFile') |
||||
if (!currentFile) return false |
||||
const extention = currentFile.substr(currentFile.length - 3, currentFile.length) |
||||
return extention.toLowerCase() === 'sol' || extention.toLowerCase() === 'yul' |
||||
} |
||||
|
||||
const sessionSwitched = () => { |
||||
if (!compileIcon.current) return |
||||
scheduleCompilation() |
||||
} |
||||
|
||||
const startingCompilation = () => { |
||||
if (!compileIcon.current) return |
||||
compileIcon.current.setAttribute('title', 'compiling...') |
||||
compileIcon.current.classList.remove('remixui_bouncingIcon') |
||||
compileIcon.current.classList.add('remixui_spinningIcon') |
||||
} |
||||
|
||||
const compilationDuration = (speed: number) => { |
||||
if (!warningIcon.current) return |
||||
if (speed > 1000) { |
||||
const msg = `Last compilation took ${speed}ms. We suggest to turn off autocompilation.` |
||||
|
||||
warningIcon.current.setAttribute('title', msg) |
||||
warningIcon.current.style.visibility = 'visible' |
||||
} else { |
||||
warningIcon.current.style.visibility = 'hidden' |
||||
} |
||||
} |
||||
|
||||
const contentChanged = () => { |
||||
if (!compileIcon.current) return |
||||
scheduleCompilation() |
||||
compileIcon.current.classList.add('remixui_bouncingIcon') // @TODO: compileView tab
|
||||
} |
||||
|
||||
const loadingCompiler = () => { |
||||
if (!compileIcon.current) return |
||||
compileIcon.current.setAttribute('title', 'compiler is loading, please wait a few moments.') |
||||
compileIcon.current.classList.add('remixui_spinningIcon') |
||||
warningIcon.current.style.visibility = 'hidden' |
||||
_updateLanguageSelector() |
||||
} |
||||
|
||||
const compilerLoaded = () => { |
||||
if (!compileIcon.current) return |
||||
compileIcon.current.setAttribute('title', '') |
||||
compileIcon.current.classList.remove('remixui_spinningIcon') |
||||
if (state.autoCompile) compile() |
||||
} |
||||
|
||||
const compilationFinished = () => { |
||||
if (!compileIcon.current) return |
||||
compileIcon.current.setAttribute('title', 'idle') |
||||
compileIcon.current.classList.remove('remixui_spinningIcon') |
||||
compileIcon.current.classList.remove('remixui_bouncingIcon') |
||||
} |
||||
|
||||
const scheduleCompilation = () => { |
||||
if (!state.autoCompile) return |
||||
if (state.compileTimeout) window.clearTimeout(state.compileTimeout) |
||||
const compileTimeout = window.setTimeout(() => { |
||||
state.autoCompile && compile() |
||||
}, state.timeout) |
||||
|
||||
setState(prevState => { |
||||
return { ...prevState, compileTimeout } |
||||
}) |
||||
} |
||||
|
||||
const compile = () => { |
||||
const currentFile = config.get('currentFile') |
||||
|
||||
if (!isSolFileSelected()) return |
||||
|
||||
_setCompilerVersionFromPragma(currentFile) |
||||
compileTabLogic.runCompiler() |
||||
} |
||||
|
||||
const _retrieveVersion = (version?) => { |
||||
if (!version) version = state.selectedVersion |
||||
if (version === 'builtin') version = state.defaultVersion |
||||
return semver.coerce(version) ? semver.coerce(version).version : '' |
||||
} |
||||
|
||||
const _updateVersionSelector = (version, customUrl = '') => { |
||||
// update selectedversion of previous one got filtered out
|
||||
let selectedVersion = version |
||||
if (!selectedVersion || !_shouldBeAdded(selectedVersion)) { |
||||
selectedVersion = state.defaultVersion |
||||
setState(prevState => { |
||||
return { ...prevState, selectedVersion } |
||||
}) |
||||
} |
||||
updateCurrentVersion(selectedVersion) |
||||
queryParams.update({ version: selectedVersion }) |
||||
let url |
||||
|
||||
if (customUrl !== '') { |
||||
selectedVersion = customUrl |
||||
setState(prevState => { |
||||
return { ...prevState, selectedVersion, customVersions: [...state.customVersions, selectedVersion] } |
||||
}) |
||||
updateCurrentVersion(selectedVersion) |
||||
url = customUrl |
||||
queryParams.update({ version: selectedVersion }) |
||||
} else if (selectedVersion === 'builtin') { |
||||
let location: string | Location = window.document.location |
||||
let path = location.pathname |
||||
if (!path.startsWith('/')) path = '/' + path |
||||
location = `${location.protocol}//${location.host}${path}assets/js` |
||||
if (location.endsWith('index.html')) location = location.substring(0, location.length - 10) |
||||
if (!location.endsWith('/')) location += '/' |
||||
url = location + 'soljson.js' |
||||
} else { |
||||
if (selectedVersion.indexOf('soljson') !== 0 || helper.checkSpecialChars(selectedVersion)) { |
||||
return console.log('loading ' + selectedVersion + ' not allowed') |
||||
} |
||||
url = `${urlFromVersion(selectedVersion)}` |
||||
} |
||||
|
||||
// Workers cannot load js on "file:"-URLs and we get a
|
||||
// "Uncaught RangeError: Maximum call stack size exceeded" error on Chromium,
|
||||
// resort to non-worker version in that case.
|
||||
if (selectedVersion !== 'builtin' && canUseWorker(selectedVersion)) { |
||||
compileTabLogic.compiler.loadVersion(true, url) |
||||
} else { |
||||
compileTabLogic.compiler.loadVersion(false, url) |
||||
} |
||||
} |
||||
|
||||
const _shouldBeAdded = (version) => { |
||||
return !version.includes('nightly') || |
||||
(version.includes('nightly') && state.includeNightlies) |
||||
} |
||||
|
||||
const promptCompiler = () => { |
||||
// custom url https://solidity-blog.s3.eu-central-1.amazonaws.com/data/08preview/soljson.js
|
||||
modal('Add a custom compiler', promptMessage('URL'), 'OK', addCustomCompiler, 'Cancel', () => {}) |
||||
} |
||||
|
||||
const promptMessage = (message) => { |
||||
return ( |
||||
<> |
||||
<span>{ message }</span> |
||||
<input type="text" data-id="modalDialogCustomPromptCompiler" className="form-control" ref={promptMessageInput} /> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
const addCustomCompiler = () => { |
||||
const url = promptMessageInput.current.value |
||||
|
||||
setState(prevState => { |
||||
return { ...prevState, selectedVersion: url } |
||||
}) |
||||
_updateVersionSelector(state.defaultVersion, url) |
||||
} |
||||
|
||||
const handleLoadVersion = (value) => { |
||||
setState(prevState => { |
||||
return { ...prevState, selectedVersion: value } |
||||
}) |
||||
updateCurrentVersion(value) |
||||
_updateVersionSelector(value) |
||||
_updateLanguageSelector() |
||||
} |
||||
|
||||
const _updateLanguageSelector = () => { |
||||
// This is the first version when Yul is available
|
||||
if (!semver.valid(_retrieveVersion()) || semver.lt(_retrieveVersion(), 'v0.5.7+commit.6da8b019.js')) { |
||||
handleLanguageChange('Solidity') |
||||
compileTabLogic.setLanguage('Solidity') |
||||
} |
||||
} |
||||
|
||||
const handleAutoCompile = (e) => { |
||||
const checked = e.target.checked |
||||
|
||||
config.set('autoCompile', checked) |
||||
setState(prevState => { |
||||
return { ...prevState, autoCompile: checked } |
||||
}) |
||||
} |
||||
|
||||
const handleOptimizeChange = (value) => { |
||||
const checked = !!value |
||||
|
||||
config.set('optimise', checked) |
||||
compileTabLogic.setOptimize(checked) |
||||
if (compileTabLogic.optimize) { |
||||
compileTabLogic.setRuns(parseInt(state.runs)) |
||||
} else { |
||||
compileTabLogic.setRuns(200) |
||||
} |
||||
state.autoCompile && compile() |
||||
setState(prevState => { |
||||
return { ...prevState, optimise: checked } |
||||
}) |
||||
} |
||||
|
||||
const onChangeRuns = (value) => { |
||||
const runs = value |
||||
|
||||
compileTabLogic.setRuns(parseInt(runs)) |
||||
state.autoCompile && compile() |
||||
setState(prevState => { |
||||
return { ...prevState, runs } |
||||
}) |
||||
} |
||||
|
||||
const handleHideWarningsChange = (e) => { |
||||
const checked = e.target.checked |
||||
|
||||
config.set('hideWarnings', checked) |
||||
state.autoCompile && compile() |
||||
setState(prevState => { |
||||
return { ...prevState, hideWarnings: checked } |
||||
}) |
||||
} |
||||
|
||||
const handleNightliesChange = (e) => { |
||||
const checked = e.target.checked |
||||
|
||||
config.set('includeNightlies', checked) |
||||
setState(prevState => { |
||||
return { ...prevState, includeNightlies: checked } |
||||
}) |
||||
} |
||||
|
||||
const handleLanguageChange = (value) => { |
||||
compileTabLogic.setLanguage(value) |
||||
state.autoCompile && compile() |
||||
setState(prevState => { |
||||
return { ...prevState, language: value } |
||||
}) |
||||
} |
||||
|
||||
const handleEvmVersionChange = (value) => { |
||||
if (!value) return |
||||
let v = value |
||||
if (v === 'default') { |
||||
v = null |
||||
} |
||||
compileTabLogic.setEvmVersion(v) |
||||
state.autoCompile && compile() |
||||
setState(prevState => { |
||||
return { ...prevState, evmVersion: value } |
||||
}) |
||||
} |
||||
|
||||
const updatehhCompilation = (event) => { |
||||
const checked = event.target.checked |
||||
|
||||
sethhCompilation(checked) |
||||
setHardHatCompilation(checked) |
||||
} |
||||
|
||||
/* |
||||
The following functions map with the above event handlers. |
||||
They are an external API for modifying the compiler configuration. |
||||
*/ |
||||
const setConfiguration = (settings: ConfigurationSettings) => { |
||||
handleLoadVersion(`soljson-v${settings.version}.js`) |
||||
handleEvmVersionChange(settings.evmVersion) |
||||
handleLanguageChange(settings.language) |
||||
handleOptimizeChange(settings.optimize) |
||||
onChangeRuns(settings.runs) |
||||
} |
||||
|
||||
return ( |
||||
<section> |
||||
<article> |
||||
<header className='remixui_compilerSection border-bottom'> |
||||
<div className="mb-2"> |
||||
<label className="remixui_compilerLabel form-check-label" htmlFor="versionSelector"> |
||||
Compiler |
||||
<button className="far fa-plus-square border-0 p-0 mx-2 btn-sm" onClick={promptCompiler} title="Add a custom compiler with URL"></button> |
||||
</label> |
||||
<select value={ state.selectedVersion || state.defaultVersion } onChange={(e) => handleLoadVersion(e.target.value) } className="custom-select" id="versionSelector" disabled={state.allversions.length <= 0}> |
||||
{ state.allversions.length <= 0 && <option disabled data-id={state.selectedVersion === state.defaultVersion ? 'selected' : ''}>{ state.defaultVersion }</option> } |
||||
{ state.allversions.length <= 0 && <option disabled data-id={state.selectedVersion === 'builtin' ? 'selected' : ''}>builtin</option> } |
||||
{ state.customVersions.map((url, i) => <option key={i} data-id={state.selectedVersion === url ? 'selected' : ''} value={url}>custom</option>)} |
||||
{ state.allversions.map((build, i) => { |
||||
return _shouldBeAdded(build.longVersion) |
||||
? <option key={i} value={build.path} data-id={state.selectedVersion === build.path ? 'selected' : ''}>{build.longVersion}</option> |
||||
: null |
||||
}) |
||||
} |
||||
</select> |
||||
</div> |
||||
<div className="mb-2 remixui_nightlyBuilds custom-control custom-checkbox"> |
||||
<input className="mr-2 custom-control-input" id="nightlies" type="checkbox" onChange={handleNightliesChange} checked={state.includeNightlies} /> |
||||
<label htmlFor="nightlies" data-id="compilerNightliesBuild" className="form-check-label custom-control-label">Include nightly builds</label> |
||||
</div> |
||||
<div className="mb-2"> |
||||
<label className="remixui_compilerLabel form-check-label" htmlFor="compilierLanguageSelector">Language</label> |
||||
<select onChange={(e) => handleLanguageChange(e.target.value)} value={state.language} className="custom-select" id="compilierLanguageSelector" title="Available since v0.5.7"> |
||||
<option value='Solidity'>Solidity</option> |
||||
<option value='Yul'>Yul</option> |
||||
</select> |
||||
</div> |
||||
<div className="mb-2"> |
||||
<label className="remixui_compilerLabel form-check-label" htmlFor="evmVersionSelector">EVM Version</label> |
||||
<select value={state.evmVersion} onChange={(e) => handleEvmVersionChange(e.target.value)} className="custom-select" id="evmVersionSelector"> |
||||
<option data-id={state.evmVersion === 'default' ? 'selected' : ''} value="default">compiler default</option> |
||||
<option data-id={state.evmVersion === 'muirGlacier' ? 'selected' : ''} value="muirGlacier">muirGlacier</option> |
||||
<option data-id={state.evmVersion === 'istanbul' ? 'selected' : ''} value="istanbul">istanbul</option> |
||||
<option data-id={state.evmVersion === 'petersburg' ? 'selected' : ''} value="petersburg">petersburg</option> |
||||
<option data-id={state.evmVersion === 'constantinople' ? 'selected' : ''} value="constantinople">constantinople</option> |
||||
<option data-id={state.evmVersion === 'byzantium' ? 'selected' : ''} value="byzantium">byzantium</option> |
||||
<option data-id={state.evmVersion === 'spuriousDragon' ? 'selected' : ''} value="spuriousDragon">spuriousDragon</option> |
||||
<option data-id={state.evmVersion === 'tangerineWhistle' ? 'selected' : ''} value="tangerineWhistle">tangerineWhistle</option> |
||||
<option data-id={state.evmVersion === 'homestead' ? 'selected' : ''} value="homestead">homestead</option> |
||||
</select> |
||||
</div> |
||||
<div className="mt-3"> |
||||
<p className="mt-2 remixui_compilerLabel">Compiler Configuration</p> |
||||
<div className="mt-2 remixui_compilerConfig custom-control custom-checkbox"> |
||||
<input className="remixui_autocompile custom-control-input" type="checkbox" onChange={handleAutoCompile} data-id="compilerContainerAutoCompile" id="autoCompile" title="Auto compile" checked={state.autoCompile} /> |
||||
<label className="form-check-label custom-control-label" htmlFor="autoCompile">Auto compile</label> |
||||
</div> |
||||
<div className="mt-2 remixui_compilerConfig custom-control custom-checkbox"> |
||||
<div className="justify-content-between align-items-center d-flex"> |
||||
<input onChange={(e) => { handleOptimizeChange(e.target.checked) }} className="custom-control-input" id="optimize" type="checkbox" checked={state.optimise} /> |
||||
<label className="form-check-label custom-control-label" htmlFor="optimize">Enable optimization</label> |
||||
<input |
||||
min="1" |
||||
className="custom-select ml-2 remixui_runs" |
||||
id="runs" |
||||
placeholder="200" |
||||
value={state.runs} |
||||
type="number" |
||||
title="Estimated number of times each opcode of the deployed code will be executed across the life-time of the contract." |
||||
onChange={(e) => onChangeRuns(e.target.value)} |
||||
disabled={!state.optimise} |
||||
/> |
||||
</div> |
||||
</div> |
||||
<div className="mt-2 remixui_compilerConfig custom-control custom-checkbox"> |
||||
<input className="remixui_autocompile custom-control-input" onChange={handleHideWarningsChange} id="hideWarningsBox" type="checkbox" title="Hide warnings" checked={state.hideWarnings} /> |
||||
<label className="form-check-label custom-control-label" htmlFor="hideWarningsBox">Hide warnings</label> |
||||
</div> |
||||
</div> |
||||
{ |
||||
isHardHatProject && <div className="mt-2 remixui_compilerConfig custom-control custom-checkbox"> |
||||
<input className="remixui_autocompile custom-control-input" onChange={updatehhCompilation} id="enableHardhat" type="checkbox" title="Enable Hardhat Compilation" checked={hhCompilation} /> |
||||
<label className="form-check-label custom-control-label" htmlFor="enableHardhat">Enable Hardhat Compilation</label> |
||||
</div> |
||||
} |
||||
<button id="compileBtn" data-id="compilerContainerCompileBtn" className="btn btn-primary btn-block remixui_disabled mt-3" title="Compile" onClick={compile} disabled={!state.compiledFileName || (state.compiledFileName && !isSolFileSelected(state.compiledFileName))}> |
||||
<span> |
||||
<i ref={warningIcon} title="Compilation Slow" style={{ visibility: 'hidden' }} className="remixui_warnCompilationSlow fas fa-exclamation-triangle" aria-hidden="true"></i> |
||||
{ warningIcon.current && warningIcon.current.style.visibility === 'hidden' && <i ref={compileIcon} className="fas fa-sync remixui_icon" aria-hidden="true"></i> } |
||||
Compile { state.compiledFileName || '<no file selected>' } |
||||
</span> |
||||
</button> |
||||
</header> |
||||
</article> |
||||
</section> |
||||
) |
||||
} |
||||
|
||||
export default CompilerContainer |
@ -0,0 +1,238 @@ |
||||
import React, { useState, useEffect } from 'react' // eslint-disable-line
|
||||
import { ContractSelectionProps } from './types' |
||||
import { PublishToStorage } from '@remix-ui/publish-to-storage' // eslint-disable-line
|
||||
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
|
||||
import { CopyToClipboard } from '@remix-ui/clipboard' // eslint-disable-line
|
||||
|
||||
import './css/style.css' |
||||
|
||||
export const ContractSelection = (props: ContractSelectionProps) => { |
||||
const { contractMap, fileProvider, fileManager, contractsDetails, modal } = props |
||||
const [contractList, setContractList] = useState([]) |
||||
const [selectedContract, setSelectedContract] = useState('') |
||||
const [storage, setStorage] = useState(null) |
||||
|
||||
useEffect(() => { |
||||
const contractList = contractMap ? Object.keys(contractMap).map((key) => ({ |
||||
name: key, |
||||
file: getFileName(contractMap[key].file) |
||||
})) : [] |
||||
|
||||
setContractList(contractList) |
||||
if (contractList.length) setSelectedContract(contractList[0].name) |
||||
}, [contractMap, contractsDetails]) |
||||
|
||||
const resetStorage = () => { |
||||
setStorage('') |
||||
} |
||||
|
||||
// Return the file name of a path: ex "browser/ballot.sol" -> "ballot.sol"
|
||||
const getFileName = (path) => { |
||||
const part = path.split('/') |
||||
|
||||
return part[part.length - 1] |
||||
} |
||||
|
||||
const handleContractChange = (contractName: string) => { |
||||
setSelectedContract(contractName) |
||||
} |
||||
|
||||
const handlePublishToStorage = (type) => { |
||||
setStorage(type) |
||||
} |
||||
|
||||
const copyABI = () => { |
||||
return copyContractProperty('abi') |
||||
} |
||||
|
||||
const copyContractProperty = (property) => { |
||||
let content = getContractProperty(property) |
||||
if (!content) { |
||||
return |
||||
} |
||||
|
||||
try { |
||||
if (typeof content !== 'string') { |
||||
content = JSON.stringify(content, null, '\t') |
||||
} |
||||
} catch (e) {} |
||||
|
||||
return content |
||||
} |
||||
|
||||
const getContractProperty = (property) => { |
||||
if (!selectedContract) throw new Error('No contract compiled yet') |
||||
const contractProperties = contractsDetails[selectedContract] |
||||
|
||||
if (contractProperties && contractProperties[property]) return contractProperties[property] |
||||
return null |
||||
} |
||||
|
||||
const renderData = (item, key: string | number, keyPath: string) => { |
||||
const data = extractData(item) |
||||
const children = (data.children || []).map((child) => renderData(child.value, child.key, keyPath + '/' + child.key)) |
||||
|
||||
if (children && children.length > 0) { |
||||
return ( |
||||
<TreeViewItem id={`treeViewItem${key}`} key={keyPath} label={ |
||||
<div className="d-flex mt-2 flex-row remixui_label_item"> |
||||
<label className="small font-weight-bold pr-1 remixui_label_key">{ key }:</label> |
||||
<label className="m-0 remixui_label_value">{ typeof data.self === 'boolean' ? `${data.self}` : data.self }</label> |
||||
</div> |
||||
}> |
||||
<TreeView id={`treeView${key}`} key={keyPath}> |
||||
{children} |
||||
</TreeView> |
||||
</TreeViewItem> |
||||
) |
||||
} else { |
||||
return <TreeViewItem id={key.toString()} key={keyPath} label={ |
||||
<div className="d-flex mt-2 flex-row remixui_label_item"> |
||||
<label className="small font-weight-bold pr-1 remixui_label_key">{ key }:</label> |
||||
<label className="m-0 remixui_label_value">{ typeof data.self === 'boolean' ? `${data.self}` : data.self }</label> |
||||
</div> |
||||
} /> |
||||
} |
||||
} |
||||
|
||||
const extractData = (item) => { |
||||
const ret = { children: null, self: null } |
||||
|
||||
if (item instanceof Array) { |
||||
ret.children = item.map((item, index) => ({ key: index, value: item })) |
||||
ret.self = '' |
||||
} else if (item instanceof Object) { |
||||
ret.children = Object.keys(item).map((key) => ({ key: key, value: item[key] })) |
||||
ret.self = '' |
||||
} else { |
||||
ret.self = item |
||||
ret.children = [] |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
const insertValue = (details, propertyName) => { |
||||
let node |
||||
if (propertyName === 'web3Deploy' || propertyName === 'name' || propertyName === 'Assembly') { |
||||
node = <pre>{ details[propertyName] }</pre> |
||||
} else if (propertyName === 'abi' || propertyName === 'metadata') { |
||||
if (details[propertyName] !== '') { |
||||
try { |
||||
node = <div> |
||||
{ (typeof details[propertyName] === 'object') |
||||
? <TreeView id="treeView"> |
||||
{ |
||||
Object.keys(details[propertyName]).map((innerkey) => renderData(details[propertyName][innerkey], innerkey, innerkey)) |
||||
} |
||||
</TreeView> : <TreeView id="treeView"> |
||||
{ |
||||
Object.keys(JSON.parse(details[propertyName])).map((innerkey) => renderData(JSON.parse(details[propertyName])[innerkey], innerkey, innerkey)) |
||||
} |
||||
</TreeView> |
||||
} |
||||
</div> // catch in case the parsing fails.
|
||||
} catch (e) { |
||||
node = <div>Unable to display "${propertyName}": ${e.message}</div> |
||||
} |
||||
} else { |
||||
node = <div> - </div> |
||||
} |
||||
} else { |
||||
node = <div>{JSON.stringify(details[propertyName], null, 4)}</div> |
||||
} |
||||
return <pre className="remixui_value">{node || ''}</pre> |
||||
} |
||||
|
||||
const details = () => { |
||||
if (!selectedContract) throw new Error('No contract compiled yet') |
||||
|
||||
const help = { |
||||
Assembly: 'Assembly opcodes describing the contract including corresponding solidity source code', |
||||
Opcodes: 'Assembly opcodes describing the contract', |
||||
'Runtime Bytecode': 'Bytecode storing the state and being executed during normal contract call', |
||||
bytecode: 'Bytecode being executed during contract creation', |
||||
functionHashes: 'List of declared function and their corresponding hash', |
||||
gasEstimates: 'Gas estimation for each function call', |
||||
metadata: 'Contains all informations related to the compilation', |
||||
metadataHash: 'Hash representing all metadata information', |
||||
abi: 'ABI: describing all the functions (input/output params, scope, ...)', |
||||
name: 'Name of the compiled contract', |
||||
swarmLocation: 'Swarm url where all metadata information can be found (contract needs to be published first)', |
||||
web3Deploy: 'Copy/paste this code to any JavaScript/Web3 console to deploy this contract' |
||||
} |
||||
const contractProperties = contractsDetails[selectedContract] || {} |
||||
const log = <div className="remixui_detailsJSON"> |
||||
{ |
||||
Object.keys(contractProperties).map((propertyName, index) => { |
||||
const copyDetails = <span className="remixui_copyDetails"><CopyToClipboard content={contractProperties[propertyName]} direction='top' /></span> |
||||
const questionMark = <span className="remixui_questionMark"><i title={ help[propertyName] } className="fas fa-question-circle" aria-hidden="true"></i></span> |
||||
|
||||
return (<div className="remixui_log" key={index}> |
||||
<div className="remixui_key">{ propertyName } { copyDetails } { questionMark }</div> |
||||
{ insertValue(contractProperties, propertyName) } |
||||
</div>) |
||||
}) |
||||
} |
||||
</div> |
||||
|
||||
modal(selectedContract, log, 'Close', null) |
||||
} |
||||
|
||||
const copyBytecode = () => { |
||||
return copyContractProperty('bytecode') |
||||
} |
||||
|
||||
return ( |
||||
// define swarm logo
|
||||
<> |
||||
{ contractList.length |
||||
? <section className="remixui_compilerSection pt-3"> |
||||
{/* Select Compiler Version */} |
||||
<div className="mb-3"> |
||||
<label className="remixui_compilerLabel form-check-label" htmlFor="compiledContracts">Contract</label> |
||||
<select onChange={(e) => handleContractChange(e.target.value)} value={selectedContract} data-id="compiledContracts" id="compiledContracts" className="custom-select"> |
||||
{ contractList.map(({ name, file }, index) => <option value={name} key={index}>{name} ({file})</option>)} |
||||
</select> |
||||
</div> |
||||
<article className="mt-2 pb-0"> |
||||
<button id="publishOnSwarm" className="btn btn-secondary btn-block" title="Publish on Swarm" onClick={() => { handlePublishToStorage('swarm') }}> |
||||
<span>Publish on Swarm</span> |
||||
<img id="swarmLogo" className="remixui_storageLogo ml-2" src="assets/img/swarm.webp" /> |
||||
</button> |
||||
<button id="publishOnIpfs" className="btn btn-secondary btn-block" title="Publish on Ipfs" onClick={() => { handlePublishToStorage('ipfs') }}> |
||||
<span>Publish on Ipfs</span> |
||||
<img id="ipfsLogo" className="remixui_storageLogo ml-2" src="assets/img/ipfs.webp" /> |
||||
</button> |
||||
<button data-id="compilation-details" className="btn btn-secondary btn-block" title="Display Contract Details" onClick={() => { details() }}> |
||||
Compilation Details |
||||
</button> |
||||
{/* Copy to Clipboard */} |
||||
<div className="remixui_contractHelperButtons"> |
||||
<div className="input-group"> |
||||
<div className="btn-group" role="group" aria-label="Copy to Clipboard"> |
||||
<CopyToClipboard title="Copy ABI to clipboard" content={copyABI()} direction='top'> |
||||
<button className="btn remixui_copyButton" title="Copy ABI to clipboard"> |
||||
<i className="remixui_copyIcon far fa-copy" aria-hidden="true"></i> |
||||
<span>ABI</span> |
||||
</button> |
||||
</CopyToClipboard> |
||||
<CopyToClipboard title="Copy ABI to clipboard" content={copyBytecode()} direction='top'> |
||||
<button className="btn remixui_copyButton" title="Copy Bytecode to clipboard"> |
||||
<i className="remixui_copyIcon far fa-copy" aria-hidden="true"></i> |
||||
<span>Bytecode</span> |
||||
</button> |
||||
</CopyToClipboard> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</article> |
||||
</section> : <section className="remixui_container clearfix"><article className="px-2 mt-2 pb-0 d-flex w-100"> |
||||
<span className="mt-2 mx-3 w-100 alert alert-warning" role="alert">No Contract Compiled Yet</span> |
||||
</article></section> |
||||
} |
||||
<PublishToStorage storage={storage} fileManager={fileManager} fileProvider={fileProvider} contract={contractsDetails[selectedContract]} resetStorage={resetStorage} /> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
export default ContractSelection |
@ -0,0 +1,234 @@ |
||||
.remixui_title { |
||||
font-size: 1.1em; |
||||
font-weight: bold; |
||||
margin-bottom: 1em; |
||||
} |
||||
.remixui_panicError { |
||||
color: red; |
||||
font-size: 20px; |
||||
} |
||||
.remixui_crow { |
||||
display: flex; |
||||
overflow: auto; |
||||
clear: both; |
||||
padding: .2em; |
||||
} |
||||
.remixui_checkboxText { |
||||
font-weight: normal; |
||||
} |
||||
.remixui_crow label { |
||||
cursor:pointer; |
||||
} |
||||
.remixui_crowNoFlex { |
||||
overflow: auto; |
||||
clear: both; |
||||
} |
||||
.remixui_info { |
||||
padding: 10px; |
||||
word-break: break-word; |
||||
} |
||||
.remixui_contract { |
||||
display: block; |
||||
margin: 3% 0; |
||||
} |
||||
.remixui_nightlyBuilds { |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
} |
||||
.remixui_autocompileContainer { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.remixui_runs { |
||||
width: 40%; |
||||
} |
||||
.remixui_hideWarningsContainer { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.remixui_autocompile {} |
||||
.remixui_autocompileTitle { |
||||
font-weight: bold; |
||||
margin: 1% 0; |
||||
} |
||||
.remixui_autocompileText { |
||||
margin: 1% 0; |
||||
font-size: 12px; |
||||
overflow: hidden; |
||||
word-break: normal; |
||||
line-height: initial; |
||||
} |
||||
.remixui_warnCompilationSlow { |
||||
margin-left: 1%; |
||||
} |
||||
.remixui_compilerConfig { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.remixui_compilerConfig label { |
||||
margin: 0; |
||||
} |
||||
.remixui_compilerSection { |
||||
padding: 12px 24px 16px; |
||||
} |
||||
.remixui_compilerLabel { |
||||
margin-bottom: 2px; |
||||
font-size: 11px; |
||||
line-height: 12px; |
||||
text-transform: uppercase; |
||||
} |
||||
.remixui_copyButton { |
||||
padding: 6px; |
||||
font-weight: bold; |
||||
font-size: 11px; |
||||
line-height: 15px; |
||||
} |
||||
.remixui_name { |
||||
display: flex; |
||||
} |
||||
.remixui_size { |
||||
display: flex; |
||||
} |
||||
.remixui_checkboxes { |
||||
display: flex; |
||||
width: 100%; |
||||
justify-content: space-between; |
||||
flex-wrap: wrap; |
||||
} |
||||
.remixui_compileButton { |
||||
width: 100%; |
||||
margin: 15px 0 10px 0; |
||||
font-size: 12px; |
||||
} |
||||
.remixui_container { |
||||
margin: 0; |
||||
margin-bottom: 2%; |
||||
} |
||||
.remixui_optimizeContainer { |
||||
display: flex; |
||||
} |
||||
.remixui_noContractAlert { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
} |
||||
.remixui_contractHelperButtons { |
||||
margin-top: 6px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
float: right; |
||||
} |
||||
.remixui_copyToClipboard { |
||||
font-size: 1rem; |
||||
} |
||||
.remixui_copyIcon { |
||||
margin-right: 5px; |
||||
} |
||||
.remixui_log { |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-bottom: 5%; |
||||
overflow: visible; |
||||
} |
||||
.remixui_key { |
||||
margin-right: 5px; |
||||
text-transform: uppercase; |
||||
width: 100%; |
||||
} |
||||
.remixui_value { |
||||
display: flex; |
||||
width: 100%; |
||||
margin-top: 1.5%; |
||||
} |
||||
.remixui_questionMark { |
||||
margin-left: 2%; |
||||
cursor: pointer; |
||||
} |
||||
.remixui_questionMark:hover { |
||||
} |
||||
.remixui_detailsJSON { |
||||
padding: 8px 0; |
||||
border: none; |
||||
} |
||||
.remixui_icon { |
||||
margin-right: 0.3em; |
||||
} |
||||
.remixui_errorBlobs { |
||||
padding-left: 5px; |
||||
padding-right: 5px; |
||||
word-break: break-word; |
||||
} |
||||
.remixui_storageLogo { |
||||
width: 20px; |
||||
height: 20px; |
||||
} |
||||
.remixui_spinningIcon { |
||||
display: inline-block; |
||||
position: relative; |
||||
animation: spin 2s infinite linear; |
||||
-moz-animation: spin 2s infinite linear; |
||||
-o-animation: spin 2s infinite linear; |
||||
-webkit-animation: spin 2s infinite linear; |
||||
} |
||||
@keyframes spin { |
||||
0% { transform: rotate(0deg); } |
||||
100% { transform: rotate(360deg); } |
||||
} |
||||
@-webkit-keyframes spin { |
||||
0% { transform: rotate(0deg); } |
||||
100% { transform: rotate(360deg); } |
||||
} |
||||
@-moz-keyframes spin { |
||||
0% { transform: rotate(0deg); } |
||||
100% { transform: rotate(360deg); } |
||||
} |
||||
@-o-keyframes spin { |
||||
0% { transform: rotate(0deg); } |
||||
100% { transform: rotate(360deg); } |
||||
} |
||||
@-ms-keyframes spin { |
||||
0% { transform: rotate(0deg); } |
||||
100% { transform: rotate(360deg); } |
||||
} |
||||
|
||||
.remixui_bouncingIcon { |
||||
display: inline-block; |
||||
position: relative; |
||||
-moz-animation: bounce 2s infinite linear; |
||||
-o-animation: bounce 2s infinite linear; |
||||
-webkit-animation: bounce 2s infinite linear; |
||||
animation: bounce 2s infinite linear; |
||||
} |
||||
|
||||
@-webkit-keyframes bounce { |
||||
0% { top: 0; } |
||||
50% { top: -0.2em; } |
||||
70% { top: -0.3em; } |
||||
100% { top: 0; } |
||||
} |
||||
@-moz-keyframes bounce { |
||||
0% { top: 0; } |
||||
50% { top: -0.2em; } |
||||
70% { top: -0.3em; } |
||||
100% { top: 0; } |
||||
} |
||||
@-o-keyframes bounce { |
||||
0% { top: 0; } |
||||
50% { top: -0.2em; } |
||||
70% { top: -0.3em; } |
||||
100% { top: 0; } |
||||
} |
||||
@-ms-keyframes bounce { |
||||
0% { top: 0; } |
||||
50% { top: -0.2em; } |
||||
70% { top: -0.3em; } |
||||
100% { top: 0; } |
||||
} |
||||
@keyframes bounce { |
||||
0% { top: 0; } |
||||
50% { top: -0.2em; } |
||||
70% { top: -0.3em; } |
||||
100% { top: 0; } |
||||
} |
@ -0,0 +1,128 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
|
||||
const packageJson = require('../../../../../../package.json') |
||||
const Compiler = require('@remix-project/remix-solidity').Compiler |
||||
const EventEmitter = require('events') |
||||
const profile = { |
||||
name: 'solidity-logic', |
||||
displayName: 'Solidity compiler logic', |
||||
description: 'Compile solidity contracts - Logic', |
||||
version: packageJson.version |
||||
} |
||||
export class CompileTab extends Plugin { |
||||
public compiler |
||||
public optimize |
||||
public runs |
||||
public evmVersion: string |
||||
public compilerImport |
||||
public event |
||||
|
||||
constructor (public queryParams, public fileManager, public editor, public config, public fileProvider, public contentImport) { |
||||
super(profile) |
||||
this.event = new EventEmitter() |
||||
this.compiler = new Compiler((url, cb) => this.call('contentImport', 'resolveAndSave', url).then((result) => cb(null, result)).catch((error) => cb(error.message))) |
||||
} |
||||
|
||||
init () { |
||||
this.optimize = this.queryParams.get().optimize |
||||
this.optimize = this.optimize === 'true' |
||||
this.queryParams.update({ optimize: this.optimize }) |
||||
this.compiler.set('optimize', this.optimize) |
||||
|
||||
this.runs = this.queryParams.get().runs |
||||
this.runs = this.runs && this.runs !== 'undefined' ? this.runs : 200 |
||||
this.queryParams.update({ runs: this.runs }) |
||||
this.compiler.set('runs', this.runs) |
||||
|
||||
this.evmVersion = this.queryParams.get().evmVersion |
||||
if (this.evmVersion === 'undefined' || this.evmVersion === 'null' || !this.evmVersion) { |
||||
this.evmVersion = null |
||||
} |
||||
this.queryParams.update({ evmVersion: this.evmVersion }) |
||||
this.compiler.set('evmVersion', this.evmVersion) |
||||
} |
||||
|
||||
setOptimize (newOptimizeValue) { |
||||
this.optimize = newOptimizeValue |
||||
this.queryParams.update({ optimize: this.optimize }) |
||||
this.compiler.set('optimize', this.optimize) |
||||
} |
||||
|
||||
setRuns (runs) { |
||||
this.runs = runs |
||||
this.queryParams.update({ runs: this.runs }) |
||||
this.compiler.set('runs', this.runs) |
||||
} |
||||
|
||||
setEvmVersion (newEvmVersion) { |
||||
this.evmVersion = newEvmVersion |
||||
this.queryParams.update({ evmVersion: this.evmVersion }) |
||||
this.compiler.set('evmVersion', this.evmVersion) |
||||
} |
||||
|
||||
/** |
||||
* Set the compiler to using Solidity or Yul (default to Solidity) |
||||
* @params lang {'Solidity' | 'Yul'} ... |
||||
*/ |
||||
setLanguage (lang) { |
||||
this.compiler.set('language', lang) |
||||
} |
||||
|
||||
/** |
||||
* Compile a specific file of the file manager |
||||
* @param {string} target the path to the file to compile |
||||
*/ |
||||
compileFile (target) { |
||||
if (!target) throw new Error('No target provided for compiliation') |
||||
const provider = this.fileManager.fileProviderOf(target) |
||||
if (!provider) throw new Error(`cannot compile ${target}. Does not belong to any explorer`) |
||||
return new Promise((resolve, reject) => { |
||||
provider.get(target, (error, content) => { |
||||
if (error) return reject(error) |
||||
const sources = { [target]: { content } } |
||||
this.event.emit('startingCompilation') |
||||
// setTimeout fix the animation on chrome... (animation triggered by 'staringCompilation')
|
||||
setTimeout(() => { this.compiler.compile(sources, target); resolve(true) }, 100) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
async isHardhatProject () { |
||||
if (this.fileManager.mode === 'localhost') { |
||||
return await this.fileManager.exists('hardhat.config.js') |
||||
} else return false |
||||
} |
||||
|
||||
runCompiler (hhCompilation) { |
||||
try { |
||||
if (this.fileManager.mode === 'localhost' && hhCompilation) { |
||||
const { currentVersion, optimize, runs } = this.compiler.state |
||||
if (currentVersion) { |
||||
const fileContent = `module.exports = {
|
||||
solidity: '${currentVersion.substring(0, currentVersion.indexOf('+commit'))}', |
||||
settings: { |
||||
optimizer: { |
||||
enabled: ${optimize}, |
||||
runs: ${runs} |
||||
} |
||||
} |
||||
} |
||||
` |
||||
const configFilePath = 'remix-compiler.config.js' |
||||
this.fileManager.setFileContent(configFilePath, fileContent) |
||||
this.call('hardhat', 'compile', configFilePath).then((result) => { |
||||
this.call('terminal', 'log', { type: 'info', value: result }) |
||||
}).catch((error) => { |
||||
this.call('terminal', 'log', { type: 'error', value: error }) |
||||
}) |
||||
} |
||||
} |
||||
this.fileManager.saveCurrentFile() |
||||
this.event.emit('removeAnnotations') |
||||
var currentFile = this.config.get('currentFile') |
||||
return this.compileFile(currentFile) |
||||
} catch (err) { |
||||
console.error(err) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
'use strict' |
||||
import * as remixLib from '@remix-project/remix-lib' |
||||
|
||||
const txHelper = remixLib.execution.txHelper |
||||
|
||||
export class CompilerAbstract { |
||||
public languageversion: string |
||||
public data: Record<string, any> |
||||
public source: Record<string, any> |
||||
|
||||
constructor (languageversion, data, source) { |
||||
this.languageversion = languageversion |
||||
this.data = data |
||||
this.source = source // source code
|
||||
} |
||||
|
||||
getContracts () { |
||||
return this.data.contracts |
||||
} |
||||
|
||||
getContract (name) { |
||||
return txHelper.getContract(name, this.data.contracts) |
||||
} |
||||
|
||||
visitContracts (calllback) { |
||||
return txHelper.visitContracts(this.data.contracts, calllback) |
||||
} |
||||
|
||||
getData () { |
||||
return this.data |
||||
} |
||||
|
||||
getAsts () { |
||||
return this.data.sources // ast
|
||||
} |
||||
|
||||
getSourceName (fileIndex) { |
||||
if (this.data && this.data.sources) { |
||||
return Object.keys(this.data.sources)[fileIndex] |
||||
} else if (Object.keys(this.source.sources).length === 1) { |
||||
// if we don't have ast, we return the only one filename present.
|
||||
const sourcesArray = Object.keys(this.source.sources) |
||||
return sourcesArray[0] |
||||
} |
||||
return null |
||||
} |
||||
|
||||
getSourceCode () { |
||||
return this.source |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
'use strict' |
||||
import { canUseWorker, urlFromVersion } from './compiler-utils' |
||||
import { Compiler } from '@remix-project/remix-solidity' |
||||
import { CompilerAbstract } from './compiler-abstract' |
||||
|
||||
export const compile = async (compilationTargets, settings, contentResolverCallback) => { |
||||
return new Promise((resolve) => { |
||||
const compiler = new Compiler(contentResolverCallback) |
||||
compiler.set('evmVersion', settings.evmVersion) |
||||
compiler.set('optimize', settings.optimize) |
||||
compiler.set('language', settings.language) |
||||
compiler.set('runs', settings.runs) |
||||
compiler.loadVersion(canUseWorker(settings.version), urlFromVersion(settings.version)) |
||||
compiler.event.register('compilationFinished', (success, compilationData, source) => { |
||||
resolve(new CompilerAbstract(settings.version, compilationData, source)) |
||||
}) |
||||
compiler.event.register('compilerLoaded', () => compiler.compile(compilationTargets, '')) |
||||
}) |
||||
} |
@ -0,0 +1,46 @@ |
||||
const semver = require('semver') |
||||
const minixhr = require('minixhr') |
||||
/* global Worker */ |
||||
|
||||
export const baseURLBin = 'https://binaries.soliditylang.org/bin' |
||||
export const baseURLWasm = 'https://binaries.soliditylang.org/wasm' |
||||
|
||||
export const pathToURL = {} |
||||
|
||||
/** |
||||
* Retrieves the URL of the given compiler version |
||||
* @param version is the version of compiler with or without 'soljson-v' prefix and .js postfix |
||||
*/ |
||||
export function urlFromVersion (version) { |
||||
if (!version.startsWith('soljson-v')) version = 'soljson-v' + version |
||||
if (!version.endsWith('.js')) version = version + '.js' |
||||
return `${pathToURL[version]}/${version}` |
||||
} |
||||
|
||||
/** |
||||
* Checks if the worker can be used to load a compiler. |
||||
* checks a compiler whitelist, browser support and OS. |
||||
*/ |
||||
export function canUseWorker (selectedVersion) { |
||||
const version = semver.coerce(selectedVersion) |
||||
const isNightly = selectedVersion.includes('nightly') |
||||
return browserSupportWorker() && ( |
||||
// All compiler versions (including nightlies) after 0.6.3 are wasm compiled
|
||||
semver.gt(version, '0.6.3') || |
||||
// Only releases are wasm compiled starting with 0.3.6
|
||||
(semver.gte(version, '0.3.6') && !isNightly) |
||||
) |
||||
} |
||||
|
||||
function browserSupportWorker () { |
||||
return document.location.protocol !== 'file:' && Worker !== undefined |
||||
} |
||||
|
||||
// returns a promise for minixhr
|
||||
export function promisedMiniXhr (url) { |
||||
return new Promise((resolve) => { |
||||
minixhr(url, (json, event) => { |
||||
resolve({ json, event }) |
||||
}) |
||||
}) |
||||
} |
@ -0,0 +1,119 @@ |
||||
'use strict' |
||||
import * as solcTranslate from 'solc/translate' |
||||
import * as remixLib from '@remix-project/remix-lib' |
||||
|
||||
const txHelper = remixLib.execution.txHelper |
||||
|
||||
export function parseContracts (contractName, contract, source) { |
||||
const detail: Record<string, any> = {} |
||||
|
||||
detail.name = contractName |
||||
detail.metadata = contract.metadata |
||||
if (contract.evm.bytecode.object) { |
||||
detail.bytecode = contract.evm.bytecode.object |
||||
} |
||||
|
||||
detail.abi = contract.abi |
||||
|
||||
if (contract.evm.bytecode.object) { |
||||
detail.bytecode = contract.evm.bytecode |
||||
detail.web3Deploy = gethDeploy(contractName.toLowerCase(), contract.abi, contract.evm.bytecode.object) |
||||
|
||||
detail.metadataHash = retrieveMetadataHash(contract.evm.bytecode.object) |
||||
if (detail.metadataHash) { |
||||
detail.swarmLocation = 'bzzr://' + detail.metadataHash |
||||
} |
||||
} |
||||
|
||||
detail.functionHashes = {} |
||||
for (const fun in contract.evm.methodIdentifiers) { |
||||
detail.functionHashes[contract.evm.methodIdentifiers[fun]] = fun |
||||
} |
||||
|
||||
detail.gasEstimates = formatGasEstimates(contract.evm.gasEstimates) |
||||
|
||||
detail.devdoc = contract.devdoc |
||||
detail.userdoc = contract.userdoc |
||||
|
||||
if (contract.evm.deployedBytecode && contract.evm.deployedBytecode.object.length > 0) { |
||||
detail['Runtime Bytecode'] = contract.evm.deployedBytecode |
||||
} |
||||
|
||||
if (source && contract.assembly !== null) { |
||||
detail.Assembly = solcTranslate.prettyPrintLegacyAssemblyJSON(contract.evm.legacyAssembly, source.content) |
||||
} |
||||
|
||||
return detail |
||||
} |
||||
|
||||
const retrieveMetadataHash = function (bytecode) { |
||||
var match = /a165627a7a72305820([0-9a-f]{64})0029$/.exec(bytecode) |
||||
if (!match) { |
||||
match = /a265627a7a72305820([0-9a-f]{64})6c6578706572696d656e74616cf50037$/.exec(bytecode) |
||||
} |
||||
if (match) { |
||||
return match[1] |
||||
} |
||||
} |
||||
|
||||
const gethDeploy = function (contractName, jsonInterface, bytecode) { |
||||
let code = '' |
||||
const funABI = txHelper.getConstructorInterface(jsonInterface) |
||||
|
||||
funABI.inputs.forEach(function (inp) { |
||||
code += 'var ' + inp.name + ' = /* var of type ' + inp.type + ' here */ ;\n' |
||||
}) |
||||
|
||||
contractName = contractName.replace(/[:./]/g, '_') |
||||
code += 'var ' + contractName + 'Contract = new web3.eth.Contract(' + JSON.stringify(jsonInterface).replace('\n', '') + ');' + |
||||
'\nvar ' + contractName + ' = ' + contractName + 'Contract.deploy({' + |
||||
"\n data: '0x" + bytecode + "', " + |
||||
'\n arguments: [' |
||||
|
||||
funABI.inputs.forEach(function (inp) { |
||||
code += '\n ' + inp.name + ',' |
||||
}) |
||||
|
||||
code += '\n ]' + |
||||
'\n}).send({' + |
||||
'\n from: web3.eth.accounts[0], ' + |
||||
"\n gas: '4700000'" + |
||||
'\n }, function (e, contract){' + |
||||
'\n console.log(e, contract);' + |
||||
"\n if (typeof contract.address !== 'undefined') {" + |
||||
"\n console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);" + |
||||
'\n }' + |
||||
'\n })' |
||||
|
||||
return code |
||||
} |
||||
|
||||
const formatGasEstimates = function (data) { |
||||
if (!data) return {} |
||||
if (data.creation === undefined && data.external === undefined && data.internal === undefined) return {} |
||||
|
||||
const gasToText = function (g) { |
||||
return g === null ? 'unknown' : g |
||||
} |
||||
|
||||
const ret: Record<string, any> = {} |
||||
let fun |
||||
if ('creation' in data) { |
||||
ret.Creation = data.creation |
||||
} |
||||
|
||||
if ('external' in data) { |
||||
ret.External = {} |
||||
for (fun in data.external) { |
||||
ret.External[fun] = gasToText(data.external[fun]) |
||||
} |
||||
} |
||||
|
||||
if ('internal' in data) { |
||||
ret.Internal = {} |
||||
for (fun in data.internal) { |
||||
ret.Internal[fun] = gasToText(data.internal[fun]) |
||||
} |
||||
} |
||||
return ret |
||||
} |
@ -0,0 +1,5 @@ |
||||
export * from './compileTabLogic' |
||||
export * from './compiler-abstract' |
||||
export * from './compiler-helpers' |
||||
export * from './compiler-utils' |
||||
export * from './contract-parser' |
@ -0,0 +1,63 @@ |
||||
interface Action { |
||||
type: string; |
||||
payload: Record<string, any>; |
||||
} |
||||
|
||||
export const compilerInitialState = { |
||||
compiler: { |
||||
mode: '', |
||||
args: null |
||||
}, |
||||
editor: { |
||||
mode: '' |
||||
} |
||||
} |
||||
|
||||
export const compilerReducer = (state = compilerInitialState, action: Action) => { |
||||
switch (action.type) { |
||||
case 'SET_COMPILER_MODE': { |
||||
return { |
||||
...state, |
||||
compiler: { |
||||
...state.compiler, |
||||
mode: action.payload.mode, |
||||
args: action.payload.args || null |
||||
} |
||||
} |
||||
} |
||||
|
||||
case 'RESET_COMPILER_MODE': { |
||||
return { |
||||
...state, |
||||
compiler: { |
||||
...state.compiler, |
||||
mode: '', |
||||
args: null |
||||
} |
||||
} |
||||
} |
||||
|
||||
case 'SET_EDITOR_MODE': { |
||||
return { |
||||
...state, |
||||
editor: { |
||||
...state.editor, |
||||
mode: action.payload |
||||
} |
||||
} |
||||
} |
||||
|
||||
case 'RESET_EDITOR_MODE': { |
||||
return { |
||||
...state, |
||||
editor: { |
||||
...state.editor, |
||||
mode: '' |
||||
} |
||||
} |
||||
} |
||||
|
||||
default: |
||||
throw new Error() |
||||
} |
||||
} |
@ -0,0 +1,115 @@ |
||||
import React, { useState } from 'react' // eslint-disable-line
|
||||
import { SolidityCompilerProps } from './types' |
||||
import { CompilerContainer } from './compiler-container' // eslint-disable-line
|
||||
import { ContractSelection } from './contract-selection' // eslint-disable-line
|
||||
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
|
||||
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
|
||||
import { Renderer } from '@remix-ui/renderer' // eslint-disable-line
|
||||
|
||||
import './css/style.css' |
||||
|
||||
export const SolidityCompiler = (props: SolidityCompilerProps) => { |
||||
const { plugin, plugin: { editor, config, queryParams, compileTabLogic, currentFile, fileProvider, fileManager, contractsDetails, contractMap, compileErrors, isHardHatProject, setHardHatCompilation, configurationSettings } } = props |
||||
const [state, setState] = useState({ |
||||
contractsDetails: {}, |
||||
eventHandlers: {}, |
||||
loading: false, |
||||
compileTabLogic: null, |
||||
compiler: null, |
||||
toasterMsg: '', |
||||
modal: { |
||||
hide: true, |
||||
title: '', |
||||
message: null, |
||||
okLabel: '', |
||||
okFn: () => {}, |
||||
cancelLabel: '', |
||||
cancelFn: () => {}, |
||||
handleHide: null |
||||
} |
||||
}) |
||||
const [currentVersion, setCurrentVersion] = useState('') |
||||
|
||||
const toast = (message: string) => { |
||||
setState(prevState => { |
||||
return { ...prevState, toasterMsg: message } |
||||
}) |
||||
} |
||||
|
||||
const updateCurrentVersion = (value) => { |
||||
setCurrentVersion(value) |
||||
plugin.setSelectedVersion(value) |
||||
} |
||||
|
||||
const modal = async (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => { |
||||
await setState(prevState => { |
||||
return { |
||||
...prevState, |
||||
modal: { |
||||
...prevState.modal, |
||||
hide: false, |
||||
message, |
||||
title, |
||||
okLabel, |
||||
okFn, |
||||
cancelLabel, |
||||
cancelFn |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
const handleHideModal = () => { |
||||
setState(prevState => { |
||||
return { ...prevState, modal: { ...state.modal, hide: true, message: null } } |
||||
}) |
||||
} |
||||
|
||||
const panicMessage = (message: string) => ( |
||||
<div> |
||||
<i className="fas fa-exclamation-circle remixui_panicError" aria-hidden="true"></i> |
||||
The compiler returned with the following internal error: <br /> <b>{message}.<br /> |
||||
The compiler might be in a non-sane state, please be careful and do not use further compilation data to deploy to mainnet. |
||||
It is heavily recommended to use another browser not affected by this issue (Firefox is known to not be affected).</b><br /> |
||||
Please join <a href="https://gitter.im/ethereum/remix" target="blank" >remix gitter channel</a> for more information. |
||||
</div> |
||||
) |
||||
|
||||
return ( |
||||
<> |
||||
<div id="compileTabView"> |
||||
<CompilerContainer editor={editor} config={config} queryParams={queryParams} compileTabLogic={compileTabLogic} tooltip={toast} modal={modal} compiledFileName={currentFile} setHardHatCompilation={setHardHatCompilation.bind(plugin)} updateCurrentVersion={updateCurrentVersion} isHardHatProject={isHardHatProject} configurationSettings={configurationSettings} /> |
||||
<ContractSelection contractMap={contractMap} fileProvider={fileProvider} fileManager={fileManager} contractsDetails={contractsDetails} modal={modal} /> |
||||
<div className="remixui_errorBlobs p-4" data-id="compiledErrors"> |
||||
<span data-id={`compilationFinishedWith_${currentVersion}`}></span> |
||||
{ compileErrors.error && <Renderer message={compileErrors.error.formattedMessage || compileErrors.error} plugin={plugin} opt={{ type: compileErrors.error.severity || 'error', errorType: compileErrors.error.type }} config={config} editor={editor} fileManager={fileManager} /> } |
||||
{ compileErrors.error && (compileErrors.error.mode === 'panic') && modal('Error', panicMessage(compileErrors.error.formattedMessage), 'Close', null) } |
||||
{ compileErrors.errors && compileErrors.errors.length && compileErrors.errors.map((err, index) => { |
||||
if (config.get('hideWarnings')) { |
||||
if (err.severity !== 'warning') { |
||||
return <Renderer key={index} message={err.formattedMessage} plugin={plugin} opt={{ type: err.severity, errorType: err.type }} config={config} editor={editor} fileManager={fileManager} /> |
||||
} |
||||
} else { |
||||
return <Renderer key={index} message={err.formattedMessage} plugin={plugin} opt={{ type: err.severity, errorType: err.type }} config={config} editor={editor} fileManager={fileManager} /> |
||||
} |
||||
}) } |
||||
</div> |
||||
</div> |
||||
<Toaster message={state.toasterMsg} /> |
||||
<ModalDialog |
||||
id='workspacesModalDialog' |
||||
title={ state.modal.title } |
||||
message={ state.modal.message } |
||||
hide={ state.modal.hide } |
||||
okLabel={ state.modal.okLabel } |
||||
okFn={ state.modal.okFn } |
||||
cancelLabel={ state.modal.cancelLabel } |
||||
cancelFn={ state.modal.cancelFn } |
||||
handleHide={ handleHideModal }> |
||||
{ (typeof state.modal.message !== 'string') && state.modal.message } |
||||
</ModalDialog> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
export default SolidityCompiler |
@ -0,0 +1,54 @@ |
||||
export interface SolidityCompilerProps { |
||||
plugin: { |
||||
contractMap: { |
||||
file: string |
||||
} | Record<string, any> |
||||
compileErrors: any, |
||||
isHardHatProject: boolean, |
||||
queryParams: any, |
||||
compileTabLogic: any, |
||||
currentFile: string, |
||||
contractsDetails: Record<string, any>, |
||||
editor: any, |
||||
config: any, |
||||
fileProvider: any, |
||||
fileManager: any, |
||||
contentImport: any, |
||||
call: (...args) => void |
||||
on: (...args) => void, |
||||
setHardHatCompilation: (value: boolean) => void, |
||||
setSelectedVersion: (value: string) => void, |
||||
configurationSettings: ConfigurationSettings |
||||
}, |
||||
} |
||||
|
||||
export interface CompilerContainerProps { |
||||
editor: any, |
||||
config: any, |
||||
queryParams: any, |
||||
compileTabLogic: any, |
||||
tooltip: (message: string | JSX.Element) => void, |
||||
modal: (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void, |
||||
compiledFileName: string, |
||||
setHardHatCompilation: (value: boolean) => void, |
||||
updateCurrentVersion: any, |
||||
isHardHatProject: boolean, |
||||
configurationSettings: ConfigurationSettings |
||||
} |
||||
export interface ContractSelectionProps { |
||||
contractMap: { |
||||
file: string |
||||
} | Record<string, any>, |
||||
fileManager: any, |
||||
fileProvider: any, |
||||
modal: (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void, |
||||
contractsDetails: Record<string, any> |
||||
} |
||||
|
||||
export interface ConfigurationSettings { |
||||
version: string, |
||||
evmVersion: string, |
||||
language: string, |
||||
optimize: boolean, |
||||
runs: string |
||||
} |
@ -0,0 +1,16 @@ |
||||
{ |
||||
"extends": "../../../tsconfig.json", |
||||
"compilerOptions": { |
||||
"jsx": "react", |
||||
"allowJs": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true |
||||
}, |
||||
"files": [], |
||||
"include": [], |
||||
"references": [ |
||||
{ |
||||
"path": "./tsconfig.lib.json" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,13 @@ |
||||
{ |
||||
"extends": "./tsconfig.json", |
||||
"compilerOptions": { |
||||
"outDir": "../../../dist/out-tsc", |
||||
"types": ["node"] |
||||
}, |
||||
"files": [ |
||||
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", |
||||
"../../../node_modules/@nrwl/react/typings/image.d.ts" |
||||
], |
||||
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"], |
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] |
||||
} |
Loading…
Reference in new issue