diff --git a/tools/webide/packages/client/package.json b/tools/webide/packages/client/package.json index 53012c75e..d6ef4e53c 100644 --- a/tools/webide/packages/client/package.json +++ b/tools/webide/packages/client/package.json @@ -25,6 +25,7 @@ "redux": "^4.0.4", "redux-devtools": "^3.5.0", "redux-thunk": "^2.3.0", + "slugify": "^1.4.0", "styled-components": "^4.4.0", "typescript": "3.6.4" }, diff --git a/tools/webide/packages/client/src/components/configure/configure-tab.tsx b/tools/webide/packages/client/src/components/configure/configure-tab.tsx index 1292cd09c..0023f3ffc 100644 --- a/tools/webide/packages/client/src/components/configure/configure-tab.tsx +++ b/tools/webide/packages/client/src/components/configure/configure-tab.tsx @@ -7,15 +7,17 @@ import { DeployAction } from '../../redux/actions/deploy'; import { DryRunAction } from '../../redux/actions/dry-run'; import { EvaluateFunctionAction } from '../../redux/actions/evaluate-function'; import { EvaluateValueAction } from '../../redux/actions/evaluate-value'; +import { GenerateCommandAction } from '../../redux/actions/generate-command'; import { AppState } from '../../redux/app'; import { ChangeDispatchedAction, ChangeSelectedAction, CommandState } from '../../redux/command'; import { Command } from '../../redux/types'; -import { CommandSelectComponent } from './command-select'; +import { Option, Select } from '../form/select'; import { CompilePaneComponent } from './compile-pane'; import { DeployPaneComponent } from './deploy-pane'; import { DryRunPaneComponent } from './dry-run-pane'; import { EvaluateFunctionPaneComponent } from './evaluate-function-pane'; import { EvaluateValuePaneComponent } from './evaluate-value-pane'; +import { GenerateCommandPaneComponent } from './generate-command-pane'; const Container = styled.div<{ visible?: boolean }>` position: absolute; @@ -58,8 +60,8 @@ const RunButton = styled.div` background-color: var(--orange); `; -const CommandPaneContainer = styled.div` - padding-top: 1em; +const SelectCommand = styled(Select)` + flex: 2; `; function createAction(command: Command) { @@ -74,6 +76,8 @@ function createAction(command: Command) { return new EvaluateValueAction(); case Command.EvaluateFunction: return new EvaluateFunctionAction(); + case Command.GenerateCommand: + return new GenerateCommandAction(); default: throw new Error('Unsupported command'); } @@ -97,12 +101,20 @@ export const ConfigureTabComponent = (props: { return ( - { dispatch({ ...new ChangeSelectedAction(command) }); }} - > + > + + + + + + + { @@ -120,23 +132,24 @@ export const ConfigureTabComponent = (props: { Run - - {(command === Command.Compile && ( - + {(command === Command.Compile && ( + + )) || + (command === Command.DryRun && ( + )) || - (command === Command.DryRun && ( - - )) || - (command === Command.Deploy && ( - - )) || - (command === Command.EvaluateFunction && ( - - )) || - (command === Command.EvaluateValue && ( - - ))} - + (command === Command.Deploy && ( + + )) || + (command === Command.EvaluateFunction && ( + + )) || + (command === Command.EvaluateValue && ( + + )) || + (command === Command.GenerateCommand && ( + + ))} ); }; diff --git a/tools/webide/packages/client/src/components/configure/generate-command-pane.tsx b/tools/webide/packages/client/src/components/configure/generate-command-pane.tsx new file mode 100644 index 000000000..da56264af --- /dev/null +++ b/tools/webide/packages/client/src/components/configure/generate-command-pane.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { AppState } from '../../redux/app'; +import { + ChangeCommandAction, + ChangeEntrypointAction, + ChangeStorageAction, + ChangeToolAction, + GenerateCommandState, +} from '../../redux/generate-command'; +import { Tool, ToolCommand } from '../../redux/types'; +import { AccessFunctionLabel, Group, Input, Label, Textarea } from '../form/inputs'; +import { Option, Select } from '../form/select'; + +const Container = styled.div` + overflow: auto; +`; + +export const GenerateCommandPaneComponent = () => { + const dispatch = useDispatch(); + + const tool = useSelector( + state => state.generateCommand.tool + ); + + const command = useSelector( + state => state.generateCommand.command + ); + + const entrypoint = useSelector( + state => state.generateCommand.entrypoint + ); + + const storage = useSelector( + state => state.generateCommand.storage + ); + + return ( + + + + + + + + + + + + + dispatch({ ...new ChangeEntrypointAction(ev.target.value) }) + } + > + + + + + + + ); +}; diff --git a/tools/webide/packages/client/src/components/editor/editor.tsx b/tools/webide/packages/client/src/components/editor/editor.tsx index f75009b07..6ed4571e0 100644 --- a/tools/webide/packages/client/src/components/editor/editor.tsx +++ b/tools/webide/packages/client/src/components/editor/editor.tsx @@ -3,11 +3,12 @@ import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { AppState } from '../../redux/app'; -import { ChangeTitleAction } from '../../redux/editor'; +import { ChangeLanguageAction, ChangeTitleAction, EditorState } from '../../redux/editor'; +import { Language } from '../../redux/types'; +import { Option, Select } from '../form/select'; import { ShareComponent } from '../share'; import { EditableTitleComponent } from './editable-title'; import { MonacoComponent } from './monaco'; -import { SyntaxSelectComponent } from './syntax-select'; const Container = styled.div` flex: 2; @@ -35,6 +36,9 @@ const StyledEditableTitleComponent = styled(EditableTitleComponent)` export const EditorComponent = () => { const dispatch = useDispatch(); const title = useSelector(state => state.editor.title); + const language = useSelector( + state => state.editor.language + ); return ( @@ -49,7 +53,17 @@ export const EditorComponent = () => { }} > - + diff --git a/tools/webide/packages/client/src/components/editor/syntax-select.tsx b/tools/webide/packages/client/src/components/editor/syntax-select.tsx deleted file mode 100644 index dcb935e7e..000000000 --- a/tools/webide/packages/client/src/components/editor/syntax-select.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { faCaretDown } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { useState } from 'react'; -import OutsideClickHandler from 'react-outside-click-handler'; -import { useDispatch, useSelector } from 'react-redux'; -import styled, { css } from 'styled-components'; - -import { AppState } from '../../redux/app'; -import { ChangeLanguageAction, EditorState } from '../../redux/editor'; -import { Language } from '../../redux/types'; -import { Tooltip } from '../tooltip'; - -const Container = styled.div` - display: flex; - position: relative; - z-index: 2; - min-width: 10em; -`; - -const Header = styled.div` - cursor: pointer; - user-select: none; - - flex: 1; - display: flex; - height: 2em; - padding: 0 0.5em; - - border: 1px solid var(--blue_trans1); -`; - -const Label = styled.div` - flex: 1; - display: flex; - align-items: center; - justify-content: space-between; -`; - -const ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => ( - -); - -const Arrow = styled(ArrowIcon)` - margin-left: 0.5em; - pointer-events: none; - color: var(--blue_trans1); - transition: transform 0.15s ease-in; - - ${(props: { rotate: boolean }) => - props.rotate && - css` - transform: rotate(180deg); - `}; -`; - -const List = styled.ul` - position: absolute; - list-style-type: none; - background-color: white; - width: 100%; - margin: 0; - padding: 0; - box-shadow: 1px 3px 10px 0px rgba(153, 153, 153, 0.4); - border-radius: 3px; - - visibility: hidden; - opacity: 0; - transition: opacity 0.15s ease-in; - - ${(props: { visible: boolean }) => - props.visible && - css` - visibility: visible; - opacity: 1; - `} -`; - -const Option = styled.li` - cursor: pointer; - user-select: none; - - display: flex; - align-items: center; - justify-content: space-between; - height: 2em; - padding: 0 0.5em; - - &:first-child { - border-radius: 3px 3px 0 0; - } - - &:last-child { - border-radius: 0 0 3px 3px; - } - - &:hover { - background-color: var(--blue_trans2); - font-weight: 600; - } -`; - -export const SyntaxSelectComponent = () => { - const OPTIONS = { - [Language.PascaLigo]: 'PascaLIGO', - [Language.CameLigo]: 'CameLIGO', - [Language.ReasonLIGO]: 'ReasonLIGO' - }; - - const moveOptionToTop = (option: Language) => { - return Object.keys(OPTIONS).reduce((list, entry) => { - if (entry === option) { - list.unshift(entry); - } else { - list.push(entry as Language); - } - return list; - }, [] as Language[]); - }; - - const language = useSelector( - state => state.editor.language - ); - const dispatch = useDispatch(); - const [opened, open] = useState(false); - - const selectOption = (option: Language) => { - if (language !== option) { - dispatch({ ...new ChangeLanguageAction(option) }); - } - open(false); - }; - - return ( - - open(false)}> - - {moveOptionToTop(language).map(option => ( - - ))} - - -
open(true)}> - - Select syntax -
-
- ); -}; diff --git a/tools/webide/packages/client/src/components/form/checkbox.tsx b/tools/webide/packages/client/src/components/form/checkbox.tsx index 8f69ddaa0..9785291e2 100644 --- a/tools/webide/packages/client/src/components/form/checkbox.tsx +++ b/tools/webide/packages/client/src/components/form/checkbox.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React, { useState } from 'react'; import styled, { css } from 'styled-components'; -const Container = styled.div<{ checked: boolean }>` +const Container = styled.div` display: flex; justify-content: center; align-items: center; @@ -44,7 +44,6 @@ export const CheckboxComponent = (props: { return ( { const newState = !isChecked; diff --git a/tools/webide/packages/client/src/components/form/inputs.tsx b/tools/webide/packages/client/src/components/form/inputs.tsx index 721a171bd..9ff057ea7 100644 --- a/tools/webide/packages/client/src/components/form/inputs.tsx +++ b/tools/webide/packages/client/src/components/form/inputs.tsx @@ -4,17 +4,20 @@ import styled from 'styled-components'; export const Group = styled.div` display: flex; flex-direction: column; + margin: 0.7em 0 0.7em 0; `; export const HGroup = styled.div` display: flex; align-items: center; + margin: 0.4em 0 0.4em 0; `; export const Label = styled.label` font-size: 1em; color: var(--label_foreground); user-select: none; + margin: 0.3em 0 0.3em 0; `; export const Hint = styled.span` @@ -25,7 +28,7 @@ export const Hint = styled.span` export const AccessFunctionLabel = (props: any) => { return ( @@ -33,7 +36,7 @@ export const AccessFunctionLabel = (props: any) => { }; export const Input = styled.input` - margin: 0.3em 0 0.7em 0; + /* margin: 0.3em 0 0.7em 0; */ background-color: var(--input_background); border-style: none; border-bottom: 5px solid #e1f1ff; @@ -49,7 +52,7 @@ export const Input = styled.input` export const Textarea = styled.textarea` resize: vertical; - margin: 0.3em 0 0.7em 0; + /* margin: 0.3em 0 0.7em 0; */ background-color: var(--input_background); border-style: none; border-bottom: 5px solid #e1f1ff; diff --git a/tools/webide/packages/client/src/components/configure/command-select.tsx b/tools/webide/packages/client/src/components/form/select.tsx similarity index 52% rename from tools/webide/packages/client/src/components/configure/command-select.tsx rename to tools/webide/packages/client/src/components/form/select.tsx index 43711dfd3..b6b9749f5 100644 --- a/tools/webide/packages/client/src/components/configure/command-select.tsx +++ b/tools/webide/packages/client/src/components/form/select.tsx @@ -1,17 +1,13 @@ import { faCaretDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { useState } from 'react'; +import React, { FunctionComponentElement, useState } from 'react'; import OutsideClickHandler from 'react-outside-click-handler'; import styled, { css } from 'styled-components'; -import { Command } from '../../redux/types'; - const Container = styled.div` - flex: 2; display: flex; position: relative; min-width: 8em; - z-index: 2; `; const Header = styled.div` @@ -20,8 +16,8 @@ const Header = styled.div` flex: 1; display: flex; - align-items: center; justify-content: space-between; + align-items: center; min-height: 2em; padding: 0 0.5em; @@ -33,6 +29,7 @@ const ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => ( ); const Arrow = styled(ArrowIcon)` + z-index: 1; pointer-events: none; color: var(--blue_trans1); transition: transform 0.15s ease-in; @@ -45,6 +42,7 @@ const Arrow = styled(ArrowIcon)` `; const List = styled.ul` + z-index: 1; position: absolute; list-style-type: none; background-color: white; @@ -66,7 +64,7 @@ const List = styled.ul` `} `; -const Option = styled.li` +const OptionContainer = styled.li` cursor: pointer; user-select: none; @@ -90,56 +88,73 @@ const Option = styled.li` } `; -export const CommandSelectComponent = (props: { - selected: Command; - onChange?: (value: Command) => void; -}) => { - const OPTIONS = { - [Command.Compile]: 'Compile', - [Command.Deploy]: 'Deploy', - [Command.DryRun]: 'Dry Run', - [Command.EvaluateFunction]: 'Evaluate Function', - [Command.EvaluateValue]: 'Evaluate Value' - }; +interface OptionProps { + value: string; + children: string; +} - const moveOptionToTop = (option: Command) => { - return Object.keys(OPTIONS).reduce((list, entry) => { - if (entry === option) { +type OptionElement = FunctionComponentElement; + +export const Option = (props: OptionProps) => { + // This is an empty component. It's used as a way to get option information into its parent. It is not inserted into the DOM. + return <>; +}; + +export const Select = (props: { + id: string; + value: any; + children: OptionElement[] | OptionElement; + onChange?: (value: any) => void; + className?: string; +}) => { + const [isOpen, setOpen] = useState(false); + + const options = Array.isArray(props.children) + ? props.children + : [props.children]; + + const labelLookup = new Map( + options.map( + child => [child.props.value, child.props.children] as [string, string] + ) + ); + + const moveOptionToTop = (value: string) => { + return options.reduce((list, entry) => { + if (entry.props.value === value) { list.unshift(entry); } else { - list.push(entry as Command); + list.push(entry); } return list; - }, [] as Command[]); + }, [] as OptionElement[]); }; - const [opened, open] = useState(false); - - const selectOption = (option: Command) => { - if (props.selected !== option && props.onChange) { - props.onChange(option); + const selectOption = (option: OptionElement) => { + if (props.value !== option.props.value && props.onChange) { + props.onChange(option.props.value); } - open(false); + setOpen(false); }; return ( - - open(false)}> - - {moveOptionToTop(props.selected).map(option => ( - + {option.props.children} + ))} -
open(true)}> - {OPTIONS[props.selected]} - +
setOpen(true)}> + {labelLookup.get(props.value)} +
); diff --git a/tools/webide/packages/client/src/components/form/toggle.tsx b/tools/webide/packages/client/src/components/form/toggle.tsx deleted file mode 100644 index a1a0b3954..000000000 --- a/tools/webide/packages/client/src/components/form/toggle.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { faCheck } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { useState } from 'react'; -import styled, { css } from 'styled-components'; - -const Container = styled.div<{ checked: boolean }>` - position: relative; - height: 2em; - width: 3.5em; - border-radius: 1em; - background-color: var(--blue_trans1); - border: 1px solid var(--blue); - transition: background-color 0.2s ease-in; - - ${props => - props.checked && - css` - background-color: var(--blue); - `}; -`; - -const Button = styled.div<{ checked: boolean }>` - display: flex; - justify-content: center; - align-items: center; - position: absolute; - - height: 2em; - width: 2em; - background-color: white; - border-radius: 50%; - cursor: pointer; - right: calc(1.5em); - transition: right 0.2s ease-in; - - ${props => - props.checked && - css` - right: 0; - `}; -`; - -const CheckIcon = ({ visible, ...props }: { visible: boolean }) => ( - -); - -const Check = styled(CheckIcon)` - position: absolute; - pointer-events: none; - opacity: 1; - transition: opacity 0.2s ease-in; - color: var(--blue); - - ${props => - !props.visible && - css` - opacity: 0; - `} -`; - -export const ToggleComponent = (props: { - checked: boolean; - onChanged: (value: boolean) => void; - className?: string; -}) => { - const [isChecked, setChecked] = useState(props.checked); - - return ( - - - - ); -}; diff --git a/tools/webide/packages/client/src/components/output/compile-output-pane.tsx b/tools/webide/packages/client/src/components/output/compile-output-pane.tsx index 110f9f54e..057892d6d 100644 --- a/tools/webide/packages/client/src/components/output/compile-output-pane.tsx +++ b/tools/webide/packages/client/src/components/output/compile-output-pane.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { AppState } from '../../redux/app'; import { ResultState } from '../../redux/result'; import { OutputToolbarComponent } from './output-toolbar'; +import { copyOutput, downloadOutput } from './utils'; const Container = styled.div<{ visible?: boolean }>` display: flex; @@ -23,35 +24,6 @@ const Pre = styled.pre` margin: 0; `; -function copyOutput(el: HTMLElement | null) { - if (el) { - const range = document.createRange(); - range.selectNodeContents(el); - - const selection = window.getSelection(); - - if (selection) { - selection.removeAllRanges(); - selection.addRange(range); - document.execCommand('copy'); - } - } -} - -function downloadOutput(output: string) { - const anchor = document.createElement('a'); - anchor.setAttribute( - 'href', - `data:text/plain;charset=utf-8,${encodeURIComponent(output)}` - ); - anchor.setAttribute('download', 'output.txt'); - - anchor.style.display = 'none'; - document.body.appendChild(anchor); - anchor.click(); - document.body.removeChild(anchor); -} - export const CompileOutputPane = () => { const output = useSelector( state => state.result.output @@ -62,6 +34,7 @@ export const CompileOutputPane = () => { return ( copyOutput(preRef.current)} onDownload={() => downloadOutput(output)} > diff --git a/tools/webide/packages/client/src/components/output/generate-command-output-pane.tsx b/tools/webide/packages/client/src/components/output/generate-command-output-pane.tsx new file mode 100644 index 000000000..5dd714375 --- /dev/null +++ b/tools/webide/packages/client/src/components/output/generate-command-output-pane.tsx @@ -0,0 +1,45 @@ +import React, { useRef } from 'react'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { AppState } from '../../redux/app'; +import { ResultState } from '../../redux/result'; +import { OutputToolbarComponent } from './output-toolbar'; +import { copyOutput, downloadOutput } from './utils'; + +const Container = styled.div<{ visible?: boolean }>` + display: flex; + flex-direction: column; + height: 100%; +`; + +const Output = styled.div` + flex: 1; + padding: 0.5em; + display: flex; + overflow: auto; +`; + +const Pre = styled.pre` + margin: 0; +`; + +export const GenerateCommandOutputPane = () => { + const output = useSelector( + state => state.result.output + ); + + const preRef = useRef(null); + + return ( + + copyOutput(preRef.current)} + onDownload={() => downloadOutput(output)} + > + +
{output}
+
+
+ ); +}; diff --git a/tools/webide/packages/client/src/components/output/output-tab.tsx b/tools/webide/packages/client/src/components/output/output-tab.tsx index 1f838aabf..5989b9f0e 100644 --- a/tools/webide/packages/client/src/components/output/output-tab.tsx +++ b/tools/webide/packages/client/src/components/output/output-tab.tsx @@ -8,6 +8,7 @@ import { ResultState } from '../../redux/result'; import { Command } from '../../redux/types'; import { CompileOutputPane } from './compile-output-pane'; import { DeployOutputPane } from './deploy-output-pane'; +import { GenerateCommandOutputPane } from './generate-command-output-pane'; import { Loading } from './loading'; import { OutputPane } from './output-pane'; @@ -41,14 +42,21 @@ export const OutputTab = (props: { const loading = useSelector( state => state.loading.loading ); + const output = useSelector( + state => state.result.output + ); const renderResult = () => { if (loading) { return ; + } else if (!output) { + return <>; } else if (command === Command.Compile) { return ; } else if (command === Command.Deploy) { return ; + } else if (command === Command.GenerateCommand) { + return ; } return ; diff --git a/tools/webide/packages/client/src/components/output/output-toolbar.tsx b/tools/webide/packages/client/src/components/output/output-toolbar.tsx index 0606b8d7a..3c064399b 100644 --- a/tools/webide/packages/client/src/components/output/output-toolbar.tsx +++ b/tools/webide/packages/client/src/components/output/output-toolbar.tsx @@ -24,6 +24,7 @@ const Link = styled.a` `; export const OutputToolbarComponent = (props: { + showTryMichelson?: boolean; onCopy?: () => void; onDownload?: () => void; }) => { @@ -41,18 +42,20 @@ export const OutputToolbarComponent = (props: { Download - - - - View in Try-Michelson IDE - - + {props.showTryMichelson && } + {props.showTryMichelson && ( + + + View in Try-Michelson IDE + + + )} ); }; diff --git a/tools/webide/packages/client/src/components/output/utils.ts b/tools/webide/packages/client/src/components/output/utils.ts new file mode 100644 index 000000000..79a5ede41 --- /dev/null +++ b/tools/webide/packages/client/src/components/output/utils.ts @@ -0,0 +1,28 @@ +export function copyOutput(el: HTMLElement | null) { + if (el) { + const range = document.createRange(); + range.selectNodeContents(el); + + const selection = window.getSelection(); + + if (selection) { + selection.removeAllRanges(); + selection.addRange(range); + document.execCommand('copy'); + } + } +} + +export function downloadOutput(output: string) { + const anchor = document.createElement('a'); + anchor.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent(output)}` + ); + anchor.setAttribute('download', 'output.txt'); + + anchor.style.display = 'none'; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); +} diff --git a/tools/webide/packages/client/src/redux/actions/generate-command.ts b/tools/webide/packages/client/src/redux/actions/generate-command.ts new file mode 100644 index 000000000..f5e2847e4 --- /dev/null +++ b/tools/webide/packages/client/src/redux/actions/generate-command.ts @@ -0,0 +1,112 @@ +import { Tezos } from '@taquito/taquito'; +import { Dispatch } from 'redux'; +import slugify from 'slugify'; + +import { compileContract, compileStorage, getErrorMessage } from '../../services/api'; +import { AppState } from '../app'; +import { MichelsonFormat } from '../compile'; +import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; +import { ChangeOutputAction } from '../result'; +import { Command } from '../types'; +import { CancellableAction } from './cancellable'; + +const URL = 'https://api.tez.ie/keys/carthagenet/'; +const AUTHORIZATION_HEADER = 'Bearer ligo-ide'; + +export async function fetchRandomPrivateKey(): Promise { + const response = await fetch(URL, { + method: 'POST', + headers: { Authorization: AUTHORIZATION_HEADER } + }); + + return response.text(); +} + +export class GenerateCommandAction extends CancellableAction { + getAction() { + return async (dispatch: Dispatch, getState: () => AppState) => { + dispatch({ ...new UpdateLoadingAction('Compiling contract...') }); + + try { + const { editor, generateCommand } = getState(); + + const michelsonCodeJson = await compileContract( + editor.language, + editor.code, + generateCommand.entrypoint, + MichelsonFormat.Json + ); + + const michelsonCode = await compileContract( + editor.language, + editor.code, + generateCommand.entrypoint + ); + + if (this.isCancelled()) { + return; + } + + dispatch({ ...new UpdateLoadingAction('Compiling storage...') }); + const michelsonStorageJson = await compileStorage( + editor.language, + editor.code, + generateCommand.entrypoint, + generateCommand.storage, + MichelsonFormat.Json + ); + + const michelsonStorage = await compileStorage( + editor.language, + editor.code, + generateCommand.entrypoint, + generateCommand.storage + ); + + if (this.isCancelled()) { + return; + } + + dispatch({ ...new UpdateLoadingAction('Estimating burn cap...') }); + + await Tezos.importKey(await fetchRandomPrivateKey()); + + const estimate = await Tezos.estimate.originate({ + code: JSON.parse(michelsonCodeJson.result), + init: JSON.parse(michelsonStorageJson.result) + }); + + if (this.isCancelled()) { + return; + } + + const title = slugify(editor.title).toLowerCase() || 'untitled'; + const output = `tezos-client \\ + ${generateCommand.command} \\ + contract \\ + ${title} \\ + transferring 0 \\ + from $YOUR_SOURCE_ACCOUNT \\ + running '${michelsonCode.result.trim()}' \\ + --init '${michelsonStorage.result.trim()}' \\ + --burn-cap ${estimate.burnFeeMutez / 1000000}`; + + dispatch({ + ...new ChangeOutputAction(output, Command.GenerateCommand) + }); + } catch (ex) { + if (this.isCancelled()) { + return; + } + dispatch({ + ...new ChangeOutputAction( + `Error: ${getErrorMessage(ex)}`, + Command.GenerateCommand + ) + }); + } + + dispatch({ ...new DoneLoadingAction() }); + }; + } +} diff --git a/tools/webide/packages/client/src/redux/app.ts b/tools/webide/packages/client/src/redux/app.ts index e7829d8c9..304af9f9b 100644 --- a/tools/webide/packages/client/src/redux/app.ts +++ b/tools/webide/packages/client/src/redux/app.ts @@ -8,6 +8,7 @@ import editor, { EditorState } from './editor'; import evaluateFunction, { EvaluateFunctionState } from './evaluate-function'; import evaluateValue, { EvaluateValueState } from './evaluate-value'; import examples, { ExamplesState } from './examples'; +import generateCommand, { GenerateCommandState } from './generate-command'; import loading, { LoadingState } from './loading'; import result, { ResultState } from './result'; import share, { ShareState } from './share'; @@ -20,6 +21,7 @@ export interface AppState { deploy: DeployState; evaluateFunction: EvaluateFunctionState; evaluateValue: EvaluateValueState; + generateCommand: GenerateCommandState; result: ResultState; command: CommandState; examples: ExamplesState; @@ -34,6 +36,7 @@ export default combineReducers({ deploy, evaluateFunction, evaluateValue, + generateCommand, result, command, examples, diff --git a/tools/webide/packages/client/src/redux/generate-command.ts b/tools/webide/packages/client/src/redux/generate-command.ts new file mode 100644 index 000000000..dadd00163 --- /dev/null +++ b/tools/webide/packages/client/src/redux/generate-command.ts @@ -0,0 +1,81 @@ +import { Tool, ToolCommand } from './types'; + +export enum ActionType { + ChangeTool = 'generate-command-change-tool', + ChangeCommand = 'generate-command-change-command', + ChangeEntrypoint = 'generate-command-change-entrypoint', + ChangeStorage = 'generate-command-change-storage' +} + +export interface GenerateCommandState { + tool: Tool; + command: ToolCommand; + entrypoint: string; + originationAccount: string; + storage: string; + burnCap: number; +} + +export class ChangeToolAction { + public readonly type = ActionType.ChangeTool; + constructor(public payload: GenerateCommandState['tool']) {} +} + +export class ChangeCommandAction { + public readonly type = ActionType.ChangeCommand; + constructor(public payload: GenerateCommandState['command']) {} +} + +export class ChangeEntrypointAction { + public readonly type = ActionType.ChangeEntrypoint; + constructor(public payload: GenerateCommandState['entrypoint']) {} +} + +export class ChangeStorageAction { + public readonly type = ActionType.ChangeStorage; + constructor(public payload: GenerateCommandState['storage']) {} +} + +type Action = + | ChangeToolAction + | ChangeCommandAction + | ChangeEntrypointAction + | ChangeStorageAction; + +const DEFAULT_STATE: GenerateCommandState = { + tool: Tool.TezosClient, + command: ToolCommand.Originate, + entrypoint: '', + storage: '', + originationAccount: '', + burnCap: 0 +}; + +export default ( + state = DEFAULT_STATE, + action: Action +): GenerateCommandState => { + switch (action.type) { + case ActionType.ChangeTool: + return { + ...state, + tool: action.payload + }; + case ActionType.ChangeCommand: + return { + ...state, + command: action.payload + }; + case ActionType.ChangeEntrypoint: + return { + ...state, + entrypoint: action.payload + }; + case ActionType.ChangeStorage: + return { + ...state, + storage: action.payload + }; + } + return state; +}; diff --git a/tools/webide/packages/client/src/redux/types.ts b/tools/webide/packages/client/src/redux/types.ts index 29484403f..3aab55c08 100644 --- a/tools/webide/packages/client/src/redux/types.ts +++ b/tools/webide/packages/client/src/redux/types.ts @@ -9,5 +9,14 @@ export enum Command { DryRun = 'dry-run', EvaluateValue = 'evaluate-value', EvaluateFunction = 'evaluate-function', - Deploy = 'deploy' + Deploy = 'deploy', + GenerateCommand = 'generate-command' +} + +export enum Tool { + TezosClient = 'tezos-client' +} + +export enum ToolCommand { + Originate = 'originate' } diff --git a/tools/webide/packages/client/src/services/api.ts b/tools/webide/packages/client/src/services/api.ts index 39defca87..128c3ff94 100644 --- a/tools/webide/packages/client/src/services/api.ts +++ b/tools/webide/packages/client/src/services/api.ts @@ -43,6 +43,9 @@ export async function compileStorage( storage: string, format?: string ) { + // For whatever reason, storage set by examples is not treated as a string. So we convert it here. + storage = `${storage}`; + const response = await axios.post('/api/compile-storage', { syntax, code, diff --git a/tools/webide/packages/server/src/handlers/compile-storage.ts b/tools/webide/packages/server/src/handlers/compile-storage.ts new file mode 100644 index 000000000..f0cc1a12b --- /dev/null +++ b/tools/webide/packages/server/src/handlers/compile-storage.ts @@ -0,0 +1,52 @@ +import joi from '@hapi/joi'; +import { Request, Response } from 'express'; + +import { CompilerError, LigoCompiler } from '../ligo-compiler'; +import { logger } from '../logger'; + +interface CompileBody { + syntax: string; + code: string; + entrypoint: string; + storage: string; + format?: string; +} + +const validateRequest = (body: any): { value: CompileBody; error: any } => { + return joi + .object({ + syntax: joi.string().required(), + code: joi.string().required(), + entrypoint: joi.string().required(), + storage: joi.string().required(), + format: joi.string().optional() + }) + .validate(body); +}; + +export async function compileStorageHandler(req: Request, res: Response) { + const { error, value: body } = validateRequest(req.body); + + if (error) { + res.status(400).json({ error: error.message }); + } else { + try { + const michelsonStorage = await new LigoCompiler().compileStorage( + body.syntax, + body.code, + body.entrypoint, + body.format || 'text', + body.storage + ); + + res.send({ result: michelsonStorage }); + } catch (ex) { + if (ex instanceof CompilerError) { + res.status(400).json({ error: ex.message }); + } else { + logger.error(ex); + res.sendStatus(500); + } + } + } +} diff --git a/tools/webide/packages/server/src/index.ts b/tools/webide/packages/server/src/index.ts index 642ec46f0..41e4678ba 100644 --- a/tools/webide/packages/server/src/index.ts +++ b/tools/webide/packages/server/src/index.ts @@ -4,6 +4,7 @@ import { dirname, join } from 'path'; import { compileContractHandler } from './handlers/compile-contract'; import { compileExpressionHandler } from './handlers/compile-expression'; +import { compileStorageHandler } from './handlers/compile-storage'; import { deployHandler } from './handlers/deploy'; import { dryRunHandler } from './handlers/dry-run'; import { evaluateValueHandler } from './handlers/evaluate-value'; @@ -51,6 +52,7 @@ app.get( ); app.post('/api/compile-contract', compileContractHandler); app.post('/api/compile-expression', compileExpressionHandler); +app.post('/api/compile-storage', compileStorageHandler); app.post('/api/dry-run', dryRunHandler); app.post('/api/share', shareHandler); app.post('/api/evaluate-value', evaluateValueHandler); diff --git a/tools/webide/yarn.lock b/tools/webide/yarn.lock index 9bc7ff8c1..c3d792c78 100644 --- a/tools/webide/yarn.lock +++ b/tools/webide/yarn.lock @@ -10706,6 +10706,11 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slugify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.4.0.tgz#c9557c653c54b0c7f7a8e786ef3431add676d2cb" + integrity sha512-FtLNsMGBSRB/0JOE2A0fxlqjI6fJsgHGS13iTuVT28kViI4JjUiNqp/vyis0ZXYcMnpR3fzGNkv+6vRlI2GwdQ== + snakeize@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d"