Merge branch 'dev' of gitlab.com:ligolang/ligo into rinderknecht@contracts

This commit is contained in:
Christian Rinderknecht 2020-03-02 10:54:26 +01:00
commit 604330eab6
16 changed files with 320 additions and 92 deletions

View File

@ -7,6 +7,7 @@ import { AppState } from '../redux/app';
import { CommandState } from '../redux/command'; import { CommandState } from '../redux/command';
import { DoneLoadingAction, LoadingState } from '../redux/loading'; import { DoneLoadingAction, LoadingState } from '../redux/loading';
import { ResultState } from '../redux/result'; import { ResultState } from '../redux/result';
import { Command } from '../redux/types';
import { OutputToolbarComponent } from './output-toolbar'; import { OutputToolbarComponent } from './output-toolbar';
const Container = styled.div<{ visible?: boolean }>` const Container = styled.div<{ visible?: boolean }>`
@ -43,7 +44,7 @@ const CancelButton = styled.div`
const Output = styled.div` const Output = styled.div`
flex: 1; flex: 1;
padding: 0 0.5em 0.5em 0.5em; padding: 0.5em;
display: flex; display: flex;
overflow: scroll; overflow: scroll;
/* This font size is used to calcuate spinner size */ /* This font size is used to calcuate spinner size */
@ -81,12 +82,11 @@ function copyOutput(el: HTMLElement | null) {
} }
} }
function downloadOutput(el: HTMLElement | null) { function downloadOutput(output: string) {
if (el) {
const anchor = document.createElement('a'); const anchor = document.createElement('a');
anchor.setAttribute( anchor.setAttribute(
'href', 'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(el.innerHTML) `data:text/plain;charset=utf-8,${encodeURIComponent(output)}`
); );
anchor.setAttribute('download', 'output.txt'); anchor.setAttribute('download', 'output.txt');
@ -95,7 +95,6 @@ function downloadOutput(el: HTMLElement | null) {
anchor.click(); anchor.click();
document.body.removeChild(anchor); document.body.removeChild(anchor);
} }
}
export const OutputTabComponent = (props: { export const OutputTabComponent = (props: {
selected?: boolean; selected?: boolean;
@ -107,6 +106,9 @@ export const OutputTabComponent = (props: {
const contract = useSelector<AppState, ResultState['contract']>( const contract = useSelector<AppState, ResultState['contract']>(
state => state.result.contract state => state.result.contract
); );
const command = useSelector<AppState, ResultState['command']>(
state => state.result.command
);
const loading = useSelector<AppState, LoadingState>(state => state.loading); const loading = useSelector<AppState, LoadingState>(state => state.loading);
@ -132,10 +134,14 @@ export const OutputTabComponent = (props: {
return ( return (
<Container visible={props.selected}> <Container visible={props.selected}>
{output.length !== 0 && ( {!(
loading.loading ||
output.length === 0 ||
command !== Command.Compile
) && (
<OutputToolbarComponent <OutputToolbarComponent
onCopy={() => copyOutput(preRef.current)} onCopy={() => copyOutput(preRef.current)}
onDownload={() => downloadOutput(preRef.current)} onDownload={() => downloadOutput(output)}
></OutputToolbarComponent> ></OutputToolbarComponent>
)} )}
<Output id="output" ref={outputRef}> <Output id="output" ref={outputRef}>

View File

@ -1,78 +1,58 @@
import { faCopy, faDownload } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faDownload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ResultState } from '../redux/result';
import { Item, Toolbar } from './toolbar';
import { Tooltip } from './tooltip'; import { Tooltip } from './tooltip';
const Container = styled.div` const Divider = styled.div`
display: flex; display: block;
justify-content: flex-start; background-color: rgba(0, 0, 0, 0.12);
padding: 0.2em 0.5em; height: 20px;
z-index: 3; width: 1px;
margin: 0 3px;
`; `;
const Action = styled.div` const Link = styled.a`
z-index: 3; font-size: 0.8em;
position: relative; color: var(--blue);
margin: 4px 6px;
cursor: pointer;
opacity: 0.5;
color: #444;
::before {
content: '';
display: block;
position: absolute;
z-index: -1;
bottom: -4px;
left: -4px;
right: -4px;
top: -4px;
border-radius: 4px;
background: none;
box-sizing: border-box;
opacity: 0;
transform: scale(0);
transition-property: transform, opacity;
transition-duration: 0.15s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
:hover::before {
background-color: rgba(32, 33, 36, 0.059);
opacity: 1; opacity: 1;
transform: scale(1);
}
:hover {
opacity: 1;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
`; `;
export const OutputToolbarComponent = (props: { export const OutputToolbarComponent = (props: {
onCopy?: () => void; onCopy?: () => void;
onDownload?: () => void; onDownload?: () => void;
}) => { }) => {
const output = useSelector<AppState, ResultState['output']>(
state => state.result.output
);
return ( return (
<Container> <Toolbar>
<Action onClick={() => props.onCopy && props.onCopy()}> <Item onClick={() => props.onCopy && props.onCopy()}>
<FontAwesomeIcon icon={faCopy}></FontAwesomeIcon> <FontAwesomeIcon icon={faCopy}></FontAwesomeIcon>
<Tooltip>Copy</Tooltip> <Tooltip>Copy</Tooltip>
</Action> </Item>
<Action onClick={() => props.onDownload && props.onDownload()}> <Item onClick={() => props.onDownload && props.onDownload()}>
<FontAwesomeIcon icon={faDownload}></FontAwesomeIcon> <FontAwesomeIcon icon={faDownload}></FontAwesomeIcon>
<Tooltip>Download</Tooltip> <Tooltip>Download</Tooltip>
</Action> </Item>
</Container> <Divider></Divider>
<Item>
<Link
target="_blank"
rel="noopener noreferrer"
href={`https://try-michelson.tzalpha.net/?source=${encodeURIComponent(
output
)}`}
>
View in Try-Michelson IDE
</Link>
</Item>
</Toolbar>
); );
}; };

View File

@ -0,0 +1,65 @@
import React from 'react';
import styled from 'styled-components';
const Container = styled.div`
display: flex;
align-items: center;
padding: 0.2em 0.5em;
z-index: 3;
`;
export const Group = styled.div`
display: flex;
align-items: center;
`;
export const Item = styled.div`
z-index: 3;
position: relative;
margin: 4px 6px;
cursor: pointer;
opacity: 0.5;
color: #444;
::before {
content: '';
display: block;
position: absolute;
z-index: -1;
bottom: -4px;
left: -4px;
right: -4px;
top: -4px;
border-radius: 4px;
background: none;
box-sizing: border-box;
opacity: 0;
transform: scale(0);
transition-property: transform, opacity;
transition-duration: 0.15s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
:hover::before {
background-color: rgba(32, 33, 36, 0.059);
opacity: 1;
transform: scale(1);
}
:hover {
opacity: 1;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
`;
export const Toolbar = (props: any) => {
return <Container>{props.children}</Container>;
};

View File

@ -48,7 +48,7 @@
--font_ghost_weight: 700; --font_ghost_weight: 700;
--font_ghost_color: rgba(153, 153, 153, 0.5); /* or #CFCFCF */ --font_ghost_color: rgba(153, 153, 153, 0.5); /* or #CFCFCF */
--content_height: 85vh; --content_height: 84vh;
--tooltip_foreground: white; --tooltip_foreground: white;
--tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/; --tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/;

View File

@ -4,6 +4,7 @@ import { compileContract, getErrorMessage } from '../../services/api';
import { AppState } from '../app'; import { AppState } from '../app';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result'; import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable'; import { CancellableAction } from './cancellable';
export class CompileAction extends CancellableAction { export class CompileAction extends CancellableAction {
@ -24,13 +25,18 @@ export class CompileAction extends CancellableAction {
return; return;
} }
dispatch({ ...new ChangeOutputAction(michelsonCode.result) }); dispatch({
...new ChangeOutputAction(michelsonCode.result, Command.Compile)
});
} catch (ex) { } catch (ex) {
if (this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
dispatch({ dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) ...new ChangeOutputAction(
`Error: ${getErrorMessage(ex)}`,
Command.Compile
)
}); });
} }

View File

@ -2,11 +2,12 @@ import { Tezos } from '@taquito/taquito';
import { TezBridgeSigner } from '@taquito/tezbridge-signer'; import { TezBridgeSigner } from '@taquito/tezbridge-signer';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { compileContract, compileExpression, deploy, getErrorMessage } from '../../services/api'; import { compileContract, compileStorage, deploy, getErrorMessage } from '../../services/api';
import { AppState } from '../app'; import { AppState } from '../app';
import { MichelsonFormat } from '../compile'; import { MichelsonFormat } from '../compile';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeContractAction, ChangeOutputAction } from '../result'; import { ChangeContractAction, ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable'; import { CancellableAction } from './cancellable';
Tezos.setProvider({ Tezos.setProvider({
@ -32,8 +33,10 @@ export class DeployAction extends CancellableAction {
} }
dispatch({ ...new UpdateLoadingAction('Compiling storage...') }); dispatch({ ...new UpdateLoadingAction('Compiling storage...') });
const michelsonStorage = await compileExpression( const michelsonStorage = await compileStorage(
editorState.language, editorState.language,
editorState.code,
deployState.entrypoint,
deployState.storage, deployState.storage,
MichelsonFormat.Json MichelsonFormat.Json
); );
@ -83,13 +86,18 @@ export class DeployAction extends CancellableAction {
return; return;
} }
dispatch({ ...new ChangeContractAction(contract.address) }); dispatch({
...new ChangeContractAction(contract.address, Command.Deploy)
});
} catch (ex) { } catch (ex) {
if (this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
dispatch({ dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) ...new ChangeOutputAction(
`Error: ${getErrorMessage(ex)}`,
Command.Deploy
)
}); });
} }

View File

@ -4,6 +4,7 @@ import { dryRun, getErrorMessage } from '../../services/api';
import { AppState } from '../app'; import { AppState } from '../app';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result'; import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable'; import { CancellableAction } from './cancellable';
export class DryRunAction extends CancellableAction { export class DryRunAction extends CancellableAction {
@ -25,13 +26,16 @@ export class DryRunAction extends CancellableAction {
if (this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
dispatch({ ...new ChangeOutputAction(result.output) }); dispatch({ ...new ChangeOutputAction(result.output, Command.DryRun) });
} catch (ex) { } catch (ex) {
if (this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
dispatch({ dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) ...new ChangeOutputAction(
`Error: ${getErrorMessage(ex)}`,
Command.DryRun
)
}); });
} }

View File

@ -4,6 +4,7 @@ import { getErrorMessage, runFunction } from '../../services/api';
import { AppState } from '../app'; import { AppState } from '../app';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result'; import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable'; import { CancellableAction } from './cancellable';
export class EvaluateFunctionAction extends CancellableAction { export class EvaluateFunctionAction extends CancellableAction {
@ -27,13 +28,18 @@ export class EvaluateFunctionAction extends CancellableAction {
if (this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
dispatch({ ...new ChangeOutputAction(result.output) }); dispatch({
...new ChangeOutputAction(result.output, Command.EvaluateFunction)
});
} catch (ex) { } catch (ex) {
if (this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
dispatch({ dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) ...new ChangeOutputAction(
`Error: ${getErrorMessage(ex)}`,
Command.EvaluateFunction
)
}); });
} }

View File

@ -4,6 +4,7 @@ import { evaluateValue, getErrorMessage } from '../../services/api';
import { AppState } from '../app'; import { AppState } from '../app';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result'; import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable'; import { CancellableAction } from './cancellable';
export class EvaluateValueAction extends CancellableAction { export class EvaluateValueAction extends CancellableAction {
@ -28,13 +29,18 @@ export class EvaluateValueAction extends CancellableAction {
return; return;
} }
dispatch({ ...new ChangeOutputAction(result.code) }); dispatch({
...new ChangeOutputAction(result.code, Command.EvaluateValue)
});
} catch (ex) { } catch (ex) {
if (this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
dispatch({ dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) ...new ChangeOutputAction(
`Error: ${getErrorMessage(ex)}`,
Command.EvaluateValue
)
}); });
} }

View File

@ -1,26 +1,36 @@
import { Command } from './types';
export enum ActionType { export enum ActionType {
ChangeOutput = 'result-change-output', ChangeOutput = 'result-change-output',
ChangeContract = 'result-change-contract' ChangeContract = 'result-change-contract'
} }
export interface ResultState { export interface ResultState {
command: Command;
output: string; output: string;
contract: string; contract: string;
} }
export class ChangeOutputAction { export class ChangeOutputAction {
public readonly type = ActionType.ChangeOutput; public readonly type = ActionType.ChangeOutput;
constructor(public payload: ResultState['output']) {} constructor(
public output: ResultState['output'],
public command: ResultState['command']
) {}
} }
export class ChangeContractAction { export class ChangeContractAction {
public readonly type = ActionType.ChangeContract; public readonly type = ActionType.ChangeContract;
constructor(public payload: ResultState['contract']) {} constructor(
public contract: ResultState['contract'],
public command: ResultState['command']
) {}
} }
type Action = ChangeOutputAction | ChangeContractAction; type Action = ChangeOutputAction | ChangeContractAction;
const DEFAULT_STATE: ResultState = { const DEFAULT_STATE: ResultState = {
command: Command.Compile,
output: '', output: '',
contract: '' contract: ''
}; };
@ -30,13 +40,15 @@ export default (state = DEFAULT_STATE, action: Action): ResultState => {
case ActionType.ChangeOutput: case ActionType.ChangeOutput:
return { return {
...state, ...state,
output: action.payload output: action.output,
command: action.command
}; };
case ActionType.ChangeContract: case ActionType.ChangeContract:
return { return {
...state, ...state,
output: DEFAULT_STATE.output, output: DEFAULT_STATE.output,
contract: action.payload contract: action.contract,
command: action.command
}; };
} }
return state; return state;

View File

@ -36,6 +36,23 @@ export async function compileExpression(
return response.data; return response.data;
} }
export async function compileStorage(
syntax: Language,
code: string,
entrypoint: string,
storage: string,
format?: string
) {
const response = await axios.post('/api/compile-storage', {
syntax,
code,
entrypoint,
storage,
format
});
return response.data;
}
export async function dryRun( export async function dryRun(
syntax: Language, syntax: Language,
code: string, code: string,

View File

@ -79,6 +79,19 @@ exports.verifyAllExamples = async (action, done) => {
done(); done();
}; };
exports.verifyWithParameter = async (command, parameter, value, action, done) => {
await page.click('#command-select');
await page.click(`#${command}`);
await page.click(`#${parameter}`);
await exports.clearText(page.keyboard);
await page.keyboard.type(value);
expect(await action()).toEqual(`Error: "${parameter}" is not allowed to be empty`);
done();
}
exports.verifyWithBlankParameter = async (command, parameter, action, done) => { exports.verifyWithBlankParameter = async (command, parameter, action, done) => {
await page.click('#command-select'); await page.click('#command-select');
await page.click(`#${command}`); await page.click(`#${command}`);

View File

@ -0,0 +1,38 @@
const commonUtils = require('./common-utils');
const API_HOST = commonUtils.API_HOST;
const runCommandAndGetOutputFor = commonUtils.runCommandAndGetOutputFor;
const clearText = commonUtils.clearText;
const COMMAND = 'deploy';
const COMMAND_ENDPOINT = 'deploy';
async function deploy() {
return await runCommandAndGetOutputFor(COMMAND, COMMAND_ENDPOINT);
}
describe('Deploy contract', () => {
beforeAll(() => jest.setTimeout(60000));
beforeEach(async () => await page.goto(API_HOST));
it('should deploy', async done => {
expect(await deploy()).toContain('The contract was successfully deployed to the babylonnet test network.');
done();
});
it('should fail to deploy contract with invalid storage', async done => {
await page.click('#command-select');
await page.click(`#deploy`);
await page.click(`#storage`);
await clearText(page.keyboard);
await page.keyboard.type('asdf');
expect(await deploy()).toContain('Error: ');
done();
});
});

View File

@ -40,10 +40,12 @@ export async function deployHandler(req: Request, res: Response) {
'json' 'json'
); );
const michelsonStorage = await new LigoCompiler().compileExpression( const michelsonStorage = await new LigoCompiler().compileStorage(
body.syntax, body.syntax,
body.storage, body.code,
'json' body.entrypoint,
'json',
body.storage
); );
await Tezos.importKey(await fetchRandomPrivateKey()); await Tezos.importKey(await fetchRandomPrivateKey());

View File

@ -120,6 +120,7 @@ export class LigoCompiler {
format: string format: string
) { ) {
const { name, remove } = await this.createTemporaryFile(code); const { name, remove } = await this.createTemporaryFile(code);
try { try {
const result = await this.execPromise(this.ligoCmd, [ const result = await this.execPromise(this.ligoCmd, [
'compile-contract', 'compile-contract',
@ -148,6 +149,33 @@ export class LigoCompiler {
return result; return result;
} }
async compileStorage(
syntax: string,
code: string,
entrypoint: string,
format: string,
storage: string
) {
const { name, remove } = await this.createTemporaryFile(code);
try {
const result = await this.execPromise(this.ligoCmd, [
'compile-storage',
'--michelson-format',
format,
'-s',
syntax,
name,
entrypoint,
storage
]);
return result;
} finally {
remove();
}
}
async dryRun( async dryRun(
syntax: string, syntax: string,
code: string, code: string,

View File

@ -0,0 +1,37 @@
import { LigoCompiler } from '../src/ligo-compiler';
const PASCALIGO_CODE = `
type action is
| Increment of int
| Decrement of int
function add (const a : int ; const b : int) : int is
block { skip } with a + b
function subtract (const a : int ; const b : int) : int is
block { skip } with a - b
function main (const p : action ; const s : int) :
(list(operation) * int) is
block { skip } with ((nil : list(operation)),
case p of
| Increment(n) -> add(s, n)
| Decrement(n) -> subtract(s, n)
end)
`;
describe('Ligo compiler', () => {
it('should compile storage', async done => {
const michelsonCode = await new LigoCompiler().compileStorage(
'pascaligo',
PASCALIGO_CODE,
'main',
'json',
'0'
);
expect(michelsonCode.trim()).toEqual('{ "int": "0" }');
done();
});
});