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

View File

@ -1,78 +1,58 @@
import { faCopy, faDownload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ResultState } from '../redux/result';
import { Item, Toolbar } from './toolbar';
import { Tooltip } from './tooltip';
const Container = styled.div`
display: flex;
justify-content: flex-start;
padding: 0.2em 0.5em;
z-index: 3;
const Divider = styled.div`
display: block;
background-color: rgba(0, 0, 0, 0.12);
height: 20px;
width: 1px;
margin: 0 3px;
`;
const Action = 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;
}
const Link = styled.a`
font-size: 0.8em;
color: var(--blue);
opacity: 1;
`;
export const OutputToolbarComponent = (props: {
onCopy?: () => void;
onDownload?: () => void;
}) => {
const output = useSelector<AppState, ResultState['output']>(
state => state.result.output
);
return (
<Container>
<Action onClick={() => props.onCopy && props.onCopy()}>
<Toolbar>
<Item onClick={() => props.onCopy && props.onCopy()}>
<FontAwesomeIcon icon={faCopy}></FontAwesomeIcon>
<Tooltip>Copy</Tooltip>
</Action>
<Action onClick={() => props.onDownload && props.onDownload()}>
</Item>
<Item onClick={() => props.onDownload && props.onDownload()}>
<FontAwesomeIcon icon={faDownload}></FontAwesomeIcon>
<Tooltip>Download</Tooltip>
</Action>
</Container>
</Item>
<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_color: rgba(153, 153, 153, 0.5); /* or #CFCFCF */
--content_height: 85vh;
--content_height: 84vh;
--tooltip_foreground: white;
--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 { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable';
export class CompileAction extends CancellableAction {
@ -24,13 +25,18 @@ export class CompileAction extends CancellableAction {
return;
}
dispatch({ ...new ChangeOutputAction(michelsonCode.result) });
dispatch({
...new ChangeOutputAction(michelsonCode.result, Command.Compile)
});
} catch (ex) {
if (this.isCancelled()) {
return;
}
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 { Dispatch } from 'redux';
import { compileContract, compileExpression, deploy, getErrorMessage } from '../../services/api';
import { compileContract, compileStorage, deploy, getErrorMessage } from '../../services/api';
import { AppState } from '../app';
import { MichelsonFormat } from '../compile';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeContractAction, ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable';
Tezos.setProvider({
@ -32,8 +33,10 @@ export class DeployAction extends CancellableAction {
}
dispatch({ ...new UpdateLoadingAction('Compiling storage...') });
const michelsonStorage = await compileExpression(
const michelsonStorage = await compileStorage(
editorState.language,
editorState.code,
deployState.entrypoint,
deployState.storage,
MichelsonFormat.Json
);
@ -83,13 +86,18 @@ export class DeployAction extends CancellableAction {
return;
}
dispatch({ ...new ChangeContractAction(contract.address) });
dispatch({
...new ChangeContractAction(contract.address, Command.Deploy)
});
} catch (ex) {
if (this.isCancelled()) {
return;
}
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 { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable';
export class DryRunAction extends CancellableAction {
@ -25,13 +26,16 @@ export class DryRunAction extends CancellableAction {
if (this.isCancelled()) {
return;
}
dispatch({ ...new ChangeOutputAction(result.output) });
dispatch({ ...new ChangeOutputAction(result.output, Command.DryRun) });
} catch (ex) {
if (this.isCancelled()) {
return;
}
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 { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable';
export class EvaluateFunctionAction extends CancellableAction {
@ -27,13 +28,18 @@ export class EvaluateFunctionAction extends CancellableAction {
if (this.isCancelled()) {
return;
}
dispatch({ ...new ChangeOutputAction(result.output) });
dispatch({
...new ChangeOutputAction(result.output, Command.EvaluateFunction)
});
} catch (ex) {
if (this.isCancelled()) {
return;
}
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 { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable';
export class EvaluateValueAction extends CancellableAction {
@ -28,13 +29,18 @@ export class EvaluateValueAction extends CancellableAction {
return;
}
dispatch({ ...new ChangeOutputAction(result.code) });
dispatch({
...new ChangeOutputAction(result.code, Command.EvaluateValue)
});
} catch (ex) {
if (this.isCancelled()) {
return;
}
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 {
ChangeOutput = 'result-change-output',
ChangeContract = 'result-change-contract'
}
export interface ResultState {
command: Command;
output: string;
contract: string;
}
export class ChangeOutputAction {
public readonly type = ActionType.ChangeOutput;
constructor(public payload: ResultState['output']) {}
constructor(
public output: ResultState['output'],
public command: ResultState['command']
) {}
}
export class ChangeContractAction {
public readonly type = ActionType.ChangeContract;
constructor(public payload: ResultState['contract']) {}
constructor(
public contract: ResultState['contract'],
public command: ResultState['command']
) {}
}
type Action = ChangeOutputAction | ChangeContractAction;
const DEFAULT_STATE: ResultState = {
command: Command.Compile,
output: '',
contract: ''
};
@ -30,13 +40,15 @@ export default (state = DEFAULT_STATE, action: Action): ResultState => {
case ActionType.ChangeOutput:
return {
...state,
output: action.payload
output: action.output,
command: action.command
};
case ActionType.ChangeContract:
return {
...state,
output: DEFAULT_STATE.output,
contract: action.payload
contract: action.contract,
command: action.command
};
}
return state;

View File

@ -36,6 +36,23 @@ export async function compileExpression(
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(
syntax: Language,
code: string,

View File

@ -79,6 +79,19 @@ exports.verifyAllExamples = async (action, 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) => {
await page.click('#command-select');
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'
);
const michelsonStorage = await new LigoCompiler().compileExpression(
const michelsonStorage = await new LigoCompiler().compileStorage(
body.syntax,
body.storage,
'json'
body.code,
body.entrypoint,
'json',
body.storage
);
await Tezos.importKey(await fetchRandomPrivateKey());

View File

@ -120,6 +120,7 @@ export class LigoCompiler {
format: string
) {
const { name, remove } = await this.createTemporaryFile(code);
try {
const result = await this.execPromise(this.ligoCmd, [
'compile-contract',
@ -148,6 +149,33 @@ export class LigoCompiler {
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(
syntax: 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();
});
});