Import webide into main ligo monorepo

When this is merged https://gitlab.com/ligolang/ligo-web-ide/ will be
marked as deprecated.

This MR does not hook up the webide build to the main CI. The CI
integration will come in a subsequent MR for the sake of making review
easier.
This commit is contained in:
Jev Björsell 2020-02-06 19:04:18 -08:00
parent dfb4c4caa3
commit c119c44c13
No known key found for this signature in database
GPG Key ID: 03F50CB91981EC9E
109 changed files with 47074 additions and 0 deletions

View File

@ -0,0 +1,10 @@
root = true
[*]
insert_final_newline = true
end_of_line = lf
[*.{js,ts,tsx,json,css}]
indent_style = space
indent_size = 2
quote_type = single

4
tools/webide/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
tmp/
dist
*.log

32
tools/webide/Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM node:12-alpine as builder
WORKDIR /app
COPY package.json package.json
COPY yarn.lock yarn.lock
COPY packages/client packages/client
COPY packages/server packages/server
RUN yarn install
COPY tsconfig.json tsconfig.json
RUN yarn workspaces run build
FROM node:12-buster
WORKDIR /app
RUN apt-get update && apt-get -y install libev-dev perl pkg-config libgmp-dev libhidapi-dev m4 libcap-dev bubblewrap rsync
COPY ligo_deb10.deb /tmp/ligo_deb10.deb
RUN dpkg -i /tmp/ligo_deb10.deb && rm /tmp/ligo_deb10.deb
COPY --from=builder /app/packages/client/build /app/client/build
COPY --from=builder /app/node_modules /app/node_modules
COPY --from=builder /app/packages/server/dist/src /app/server/dist
ENV STATIC_ASSETS /app/client
ENV LIGO_CMD /bin/ligo
ENTRYPOINT [ "node", "server/dist/index.js" ]

12
tools/webide/README.md Normal file
View File

@ -0,0 +1,12 @@
# Quick Start
Install `yarn`.
Run `yarn` to install dependencies.
## Server
See the README under the `packages/server/` for information about how to get started on the server development.
## Client
See the README under the `packages/client/` for information about how to get started on the client development.

17
tools/webide/package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "ligo-editor",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
],
"description": "",
"scripts": {
"prestart": "cd packages/client && npm run build",
"start": ""
},
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com:ligolang/ligo-web-ide.git"
}
}

23
tools/webide/packages/client/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,44 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

View File

@ -0,0 +1,37 @@
(*_*
name: Cameligo Contract
language: cameligo
compile:
entrypoint: main
dryRun:
entrypoint: main
parameters: Increment 1
storage: 0
deploy:
entrypoint: main
storage: 0
evaluateValue:
entrypoint: ""
evaluateFunction:
entrypoint: add
parameters: 5, 6
*_*)
type storage = int
(* variant defining pseudo multi-entrypoint actions *)
type action =
| Increment of int
| Decrement of int
let add (a,b: int * int) : int = a + b
let sub (a,b: int * int) : int = a - b
(* real entrypoint that re-routes the flow based on the action provided *)
let main (p,s: action * storage) =
let storage =
match p with
| Increment n -> add (s, n)
| Decrement n -> sub (s, n)
in ([] : operation list), storage

View File

@ -0,0 +1,38 @@
(*_*
name: Pascaligo Contract
language: pascaligo
compile:
entrypoint: main
dryRun:
entrypoint: main
parameters: Increment (1)
storage: 0
deploy:
entrypoint: main
storage: 0
evaluateValue:
entrypoint: ""
evaluateFunction:
entrypoint: add
parameters: (5, 6)
*_*)
// variant defining pseudo multi-entrypoint actions
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
// real entrypoint that re-routes the flow based
// on the action provided
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)

View File

@ -0,0 +1,39 @@
(*_*
name: Reasonligo Contract
language: reasonligo
compile:
entrypoint: main
dryRun:
entrypoint: main
parameters: Increment (1)
storage: 0
deploy:
entrypoint: main
storage: 0
evaluateValue:
entrypoint: ""
evaluateFunction:
entrypoint: add
parameters: (5, 6)
*_*)
type storage = int;
/* variant defining pseudo multi-entrypoint actions */
type action =
| Increment(int)
| Decrement(int);
let add = ((a,b): (int, int)): int => a + b;
let sub = ((a,b): (int, int)): int => a - b;
/* real entrypoint that re-routes the flow based on the action provided */
let main = ((p,storage): (action, storage)) => {
let storage =
switch (p) {
| Increment(n) => add((storage, n))
| Decrement(n) => sub((storage, n))
};
([]: list(operation), storage);
};

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="ligo_run.svg"
id="svg4550"
version="1.1"
viewBox="0 0 193.35434 193.35434"
height="193.35434"
width="193.35434"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
style="fill:none">
<metadata
id="metadata4554">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1400"
id="namedview4552"
showgrid="false"
inkscape:zoom="1.9624573"
inkscape:cx="24.54412"
inkscape:cy="104.17717"
inkscape:window-x="-12"
inkscape:window-y="-12"
inkscape:window-maximized="1"
inkscape:current-layer="svg4550"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<circle
cx="96.67717"
cy="96.67717"
r="74"
id="circle4541"
style="stroke:url(#paint0_linear);stroke-width:45.35433197;stroke-miterlimit:4;stroke-dasharray:none" />
<defs
id="defs4548">
<linearGradient
id="paint0_linear"
x1="100"
y1="54"
x2="100"
y2="254"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-3.322834,-57.322834)">
<stop
stop-color="#3AA0FF"
id="stop4543" />
<stop
offset="1"
stop-color="#0072DC"
id="stop4545" />
</linearGradient>
</defs>
<path
d="M 137.61977,80.627036 58.899178,68.296666 88.24752,141.00143 137.6208,80.624736 Z"
id="path4537"
inkscape:connector-curvature="0"
style="fill:#fc683a;stroke-width:2.53620434" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,118 @@
const createHash = require('crypto').createHash;
const glob = require('glob');
const join = require('path').join;
const fs = require('fs');
const YAML = require('yamljs');
function urlFriendlyHash(content) {
const hash = createHash('md5');
hash.update(content);
return hash
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
function convertToJson(content, path) {
const METADATA_REGEX = /\(\*_\*([^]*?)\*_\*\)\s*/;
const match = content.match(METADATA_REGEX);
if (!match || !match[1]) {
throw new Error(`Unable to find compiler configuration in ${path}.`);
}
try {
const config = YAML.parse(match[1]);
config.editor = {
language: config.language,
code: content.replace(METADATA_REGEX, '')
};
delete config.language;
return config;
} catch (ex) {
throw new Error(`${path} doesn't contain valid metadata. ${ex}`);
}
}
function findFiles(pattern, dir) {
return new Promise((resolve, reject) => {
glob(pattern, { cwd: dir }, (error, files) => {
if (error) {
reject(error);
} else {
resolve(files);
}
});
});
}
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (error, content) => {
if (error) {
reject(error);
} else {
resolve(content);
}
});
});
}
function writeFile(path, config) {
return new Promise((resolve, reject) => {
fs.writeFile(path, JSON.stringify(config), error => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
async function processExample(srcDir, file, destDir) {
const path = join(srcDir, file);
console.log(`Processing ${path}`);
const content = await readFile(path);
const config = convertToJson(content, path);
const id = urlFriendlyHash(file);
config.id = id;
await writeFile(join(destDir, id), config);
return { id: id, name: config.name };
}
function processExamples(srcDir, files, destDir) {
return Promise.all(files.map(file => processExample(srcDir, file, destDir)));
}
async function main() {
process.on('unhandledRejection', error => {
throw error;
});
const EXAMPLES_DEST_DIR = join(process.cwd(), 'build', 'static', 'examples');
const EXAMPLES_DIR = join(process.cwd(), 'examples');
const EXAMPLES_GLOB = '**/*.ligo';
const EXAMPLES_LIST_FILE = 'list';
fs.mkdirSync(EXAMPLES_DEST_DIR, { recursive: true });
const files = await findFiles(EXAMPLES_GLOB, EXAMPLES_DIR);
const examples = await processExamples(
EXAMPLES_DIR,
files,
EXAMPLES_DEST_DIR
);
await writeFile(join(EXAMPLES_DEST_DIR, EXAMPLES_LIST_FILE), examples);
}
main();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.6",
"@taquito/taquito": "^5.1.0-beta.1",
"@taquito/tezbridge-signer": "^5.1.0-beta.1",
"@types/jest": "24.0.18",
"@types/node": "12.7.12",
"@types/react": "16.9.5",
"@types/react-dom": "16.9.1",
"@types/react-outside-click-handler": "^1.2.0",
"axios": "^0.19.0",
"http-proxy-middleware": "^0.20.0",
"monaco-editor": "npm:@ligolang/monaco-editor@0.18.1",
"react": "^16.10.2",
"react-dom": "^16.10.2",
"react-outside-click-handler": "^1.3.0",
"react-redux": "^7.1.1",
"react-scripts": "3.2.0",
"react-spinners-kit": "^1.9.0",
"redux": "^4.0.4",
"redux-devtools": "^3.5.0",
"redux-thunk": "^2.3.0",
"styled-components": "^4.4.0",
"typescript": "3.6.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"postbuild": "node package-examples.js",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/glob": "^7.1.1",
"@types/react-redux": "^7.1.4",
"@types/styled-components": "^4.1.19",
"glob": "^7.1.6",
"node-sass": "^4.12.0",
"yamljs": "^0.3.0"
}
}

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="The LIGO Playground for learning LIGO" />
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<!-- Global site tag (gtag.js) - Google Analytics -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=UA-153751765-1"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "UA-153751765-1");
</script>
<script src="https://www.tezbridge.com/plugin.js"></script>
<title>LIGO Playground</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "LIGO Playground",
"name": "The LIGO Playground for learning LIGO",
"icons": [
{
"src": "logo.svg",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/svg+xml"
},
{
"src": "logo.svg",
"type": "image/svg+xml",
"sizes": "192x192"
},
{
"src": "logo.svg",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -0,0 +1,55 @@
import React from 'react';
import { Provider } from 'react-redux';
import styled from 'styled-components';
import { EditorComponent } from './components/editor';
import { Examples } from './components/examples';
import { FloatButtonComponent } from './components/float-button';
import { HeaderComponent } from './components/header';
import { TabsPanelComponent } from './components/tabs-panel';
import configureStore from './configure-store';
const store = configureStore();
const Container = styled.div`
display: flex;
padding: 0.5em 1em;
`;
const FeedbackContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
right: 1em;
bottom: 1em;
position: absolute;
`;
const App: React.FC = () => {
return (
<Provider store={store}>
<HeaderComponent></HeaderComponent>
<Container>
<Examples></Examples>
<EditorComponent></EditorComponent>
<TabsPanelComponent></TabsPanelComponent>
</Container>
<FeedbackContainer>
<FloatButtonComponent
tooltip="Report an issue"
text="!"
href="https://gitlab.com/ligolang/ligo-web-ide/issues"
></FloatButtonComponent>
<FloatButtonComponent
tooltip="Ask a question"
text="?"
href="https://discord.gg/9rhYaEt"
></FloatButtonComponent>
</FeedbackContainer>
</Provider>
);
};
export default App;

View File

@ -0,0 +1,58 @@
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 }>`
display: flex;
justify-content: center;
align-items: center;
height: 2.5em;
width: 2.5em;
background: var(--blue_trans2);
cursor: pointer;
`;
const CheckIcon = ({ visible, ...props }: { visible: boolean }) => (
<FontAwesomeIcon {...props} size="2x" icon={faCheck}></FontAwesomeIcon>
);
const Check = styled(CheckIcon)`
pointer-events: none;
opacity: 1;
transform: scale(1);
transition: transform 0.2s ease-in;
color: var(--orange);
${props =>
!props.visible &&
css`
transition: scale(1);
opacity: 0;
`}
`;
export const CheckboxComponent = (props: {
checked: boolean;
onChanged: (value: boolean) => void;
className?: string;
}) => {
const [isChecked, setChecked] = useState(props.checked);
return (
<Container
className={props.className}
checked={isChecked}
onClick={() => {
const newState = !isChecked;
setChecked(newState);
props.onChanged(newState);
}}
>
<Check visible={isChecked}></Check>
</Container>
);
};

View File

@ -0,0 +1,146 @@
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 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`
cursor: pointer;
user-select: none;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 2em;
padding: 0 0.5em;
border: 1px solid var(--blue_trans1);
`;
const ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => (
<FontAwesomeIcon {...props} icon={faCaretDown} size="lg"></FontAwesomeIcon>
);
const Arrow = styled(ArrowIcon)`
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 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'
};
const moveOptionToTop = (option: Command) => {
return Object.keys(OPTIONS).reduce((list, entry) => {
if (entry === option) {
list.unshift(entry);
} else {
list.push(entry as Command);
}
return list;
}, [] as Command[]);
};
const [opened, open] = useState(false);
const selectOption = (option: Command) => {
if (props.selected !== option && props.onChange) {
props.onChange(option);
}
open(false);
};
return (
<Container>
<OutsideClickHandler onOutsideClick={() => open(false)}>
<List visible={opened}>
{moveOptionToTop(props.selected).map(option => (
<Option
id={option}
key={option}
onClick={() => selectOption(option)}
>
<span>{OPTIONS[option]}</span>
</Option>
))}
</List>
</OutsideClickHandler>
<Header id="command-select" onClick={() => open(true)}>
<span>{OPTIONS[props.selected]}</span>
<Arrow rotate={opened}></Arrow>
</Header>
</Container>
);
};

View File

@ -0,0 +1,31 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeEntrypointAction, CompileState } from '../redux/compile';
import { Group, Input, Label } from './inputs';
const Container = styled.div``;
export const CompilePaneComponent = () => {
const dispatch = useDispatch();
const entrypoint = useSelector<AppState, CompileState['entrypoint']>(
state => state.compile.entrypoint
);
return (
<Container>
<Group>
<Label htmlFor="entrypoint">Entrypoint</Label>
<Input
id="entrypoint"
value={entrypoint}
onChange={ev =>
dispatch({ ...new ChangeEntrypointAction(ev.target.value) })
}
></Input>
</Group>
</Container>
);
};

View File

@ -0,0 +1,142 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled, { css } from 'styled-components';
import { CompileAction } from '../redux/actions/compile';
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 { AppState } from '../redux/app';
import { ChangeDispatchedAction, ChangeSelectedAction, CommandState } from '../redux/command';
import { Command } from '../redux/types';
import { CommandSelectComponent } from './command-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';
const Container = styled.div<{ visible?: boolean }>`
position: absolute;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 1em 1em 0 1em;
display: flex;
flex-direction: column;
transform: translateX(-100%);
transition: transform 0.2s ease-in;
${props =>
props.visible &&
css`
transform: translateX(0px);
`}
`;
const CommonActionsGroup = styled.div`
display: flex;
align-items: center;
`;
const RunButton = styled.div`
cursor: pointer;
user-select: none;
display: flex;
justify-content: center;
align-items: center;
flex: 1;
min-height: 2em;
min-width: 3em;
margin-left: 1em;
color: white;
background-color: var(--orange);
`;
const CommandPaneContainer = styled.div`
padding-top: 1em;
`;
function createAction(command: Command) {
switch (command) {
case Command.Compile:
return new CompileAction();
case Command.DryRun:
return new DryRunAction();
case Command.Deploy:
return new DeployAction();
case Command.EvaluateValue:
return new EvaluateValueAction();
case Command.EvaluateFunction:
return new EvaluateFunctionAction();
default:
throw new Error('Unsupported command');
}
}
export const ConfigureTabComponent = (props: {
selected?: boolean;
onRun?: () => void;
}) => {
const dispatchedAction = useSelector<
AppState,
CommandState['dispatchedAction']
>(state => state.command.dispatchedAction);
const command = useSelector<AppState, CommandState['selected']>(
state => state.command.selected
);
const dispatch = useDispatch();
return (
<Container visible={props.selected}>
<CommonActionsGroup>
<CommandSelectComponent
selected={command}
onChange={command => {
dispatch({ ...new ChangeSelectedAction(command) });
}}
></CommandSelectComponent>
<RunButton
id="run"
onClick={() => {
if (dispatchedAction) {
dispatchedAction.cancel();
}
const newAction = createAction(command);
dispatch(newAction.getAction());
dispatch({ ...new ChangeDispatchedAction(newAction) });
props.onRun!();
}}
>
Run
</RunButton>
</CommonActionsGroup>
<CommandPaneContainer>
{(command === Command.Compile && (
<CompilePaneComponent></CompilePaneComponent>
)) ||
(command === Command.DryRun && (
<DryRunPaneComponent></DryRunPaneComponent>
)) ||
(command === Command.Deploy && (
<DeployPaneComponent></DeployPaneComponent>
)) ||
(command === Command.EvaluateFunction && (
<EvaluateFunctionPaneComponent></EvaluateFunctionPaneComponent>
)) ||
(command === Command.EvaluateValue && (
<EvaluateValuePaneComponent></EvaluateValuePaneComponent>
))}
</CommandPaneContainer>
</Container>
);
};

View File

@ -0,0 +1,69 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeEntrypointAction, ChangeStorageAction, DeployState, UseTezBridgeAction } from '../redux/deploy';
import { CheckboxComponent } from './checkbox';
import { Group, HGroup, Input, Label, Textarea } from './inputs';
const Container = styled.div``;
const Checkbox = styled(CheckboxComponent)`
margin-right: 0.3em;
`;
const Hint = styled.span`
font-style: italic;
font-size: 0.8em;
`;
export const DeployPaneComponent = () => {
const dispatch = useDispatch();
const entrypoint = useSelector<AppState, DeployState['entrypoint']>(
state => state.deploy.entrypoint
);
const storage = useSelector<AppState, DeployState['storage']>(
state => state.deploy.storage
);
const useTezBridge = useSelector<AppState, DeployState['useTezBridge']>(
state => state.deploy.useTezBridge
);
return (
<Container>
<Group>
<Label htmlFor="entrypoint">Entrypoint</Label>
<Input
id="entrypoint"
value={entrypoint}
onChange={ev =>
dispatch({ ...new ChangeEntrypointAction(ev.target.value) })
}
></Input>
</Group>
<Group>
<Label htmlFor="storage">Storage</Label>
<Textarea
id="storage"
rows={9}
value={storage}
onChange={ev =>
dispatch({ ...new ChangeStorageAction(ev.target.value) })
}
></Textarea>
</Group>
<HGroup>
<Checkbox
checked={!useTezBridge}
onChanged={value => dispatch({ ...new UseTezBridgeAction(!value) })}
></Checkbox>
<Label htmlFor="tezbridge">
We'll sign for you
<br />
<Hint>Got your own key? Deselect to sign with TezBridge</Hint>
</Label>
</HGroup>
</Container>
);
};

View File

@ -0,0 +1,59 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeEntrypointAction, ChangeParametersAction, ChangeStorageAction, DryRunState } from '../redux/dry-run';
import { Group, Input, Label, Textarea } from './inputs';
const Container = styled.div``;
export const DryRunPaneComponent = () => {
const dispatch = useDispatch();
const entrypoint = useSelector<AppState, DryRunState['entrypoint']>(
state => state.dryRun.entrypoint
);
const parameters = useSelector<AppState, DryRunState['parameters']>(
state => state.dryRun.parameters
);
const storage = useSelector<AppState, DryRunState['storage']>(
state => state.dryRun.storage
);
return (
<Container>
<Group>
<Label htmlFor="entrypoint">Entrypoint</Label>
<Input
id="entrypoint"
value={entrypoint}
onChange={ev =>
dispatch({ ...new ChangeEntrypointAction(ev.target.value) })
}
></Input>
</Group>
<Group>
<Label htmlFor="parameters">Parameters</Label>
<Textarea
id="parameters"
rows={9}
value={parameters}
onChange={ev =>
dispatch({ ...new ChangeParametersAction(ev.target.value) })
}
></Textarea>
</Group>
<Group>
<Label htmlFor="storage">Storage</Label>
<Textarea
id="storage"
rows={9}
value={storage}
onChange={ev =>
dispatch({ ...new ChangeStorageAction(ev.target.value) })
}
></Textarea>
</Group>
</Container>
);
};

View File

@ -0,0 +1,32 @@
import React from 'react';
import styled from 'styled-components';
import { MonacoComponent } from './monaco';
import { ShareComponent } from './share';
import { SyntaxSelectComponent } from './syntax-select';
const Container = styled.div`
flex: 2;
`;
const Header = styled.div`
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 2.5em;
border-bottom: 5px solid var(--blue_trans1);
`;
export const EditorComponent = () => {
return (
<Container>
<Header>
<SyntaxSelectComponent></SyntaxSelectComponent>
<ShareComponent></ShareComponent>
</Header>
<MonacoComponent></MonacoComponent>
</Container>
);
};

View File

@ -0,0 +1,45 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeEntrypointAction, ChangeParametersAction, EvaluateFunctionState } from '../redux/evaluate-function';
import { Group, Input, Label, Textarea } from './inputs';
const Container = styled.div``;
export const EvaluateFunctionPaneComponent = () => {
const dispatch = useDispatch();
const entrypoint = useSelector<AppState, EvaluateFunctionState['entrypoint']>(
state => state.evaluateFunction.entrypoint
);
const parameters = useSelector<AppState, EvaluateFunctionState['parameters']>(
state => state.evaluateFunction.parameters
);
return (
<Container>
<Group>
<Label htmlFor="entrypoint">Entrypoint</Label>
<Input
id="entrypoint"
value={entrypoint}
onChange={ev =>
dispatch({ ...new ChangeEntrypointAction(ev.target.value) })
}
></Input>
</Group>
<Group>
<Label htmlFor="parameters">Parameters</Label>
<Textarea
id="parameters"
rows={9}
value={parameters}
onChange={ev =>
dispatch({ ...new ChangeParametersAction(ev.target.value) })
}
></Textarea>
</Group>
</Container>
);
};

View File

@ -0,0 +1,31 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeEntrypointAction, EvaluateValueState } from '../redux/evaluate-value';
import { Group, Input, Label } from './inputs';
const Container = styled.div``;
export const EvaluateValuePaneComponent = () => {
const dispatch = useDispatch();
const entrypoint = useSelector<AppState, EvaluateValueState['entrypoint']>(
state => state.evaluateValue.entrypoint
);
return (
<Container>
<Group>
<Label htmlFor="entrypoint">Entrypoint</Label>
<Input
id="entrypoint"
value={entrypoint}
onChange={ev =>
dispatch({ ...new ChangeEntrypointAction(ev.target.value) })
}
></Input>
</Group>
</Container>
);
};

View File

@ -0,0 +1,107 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeSelectedAction, ExamplesState } from '../redux/examples';
import { getExample } from '../services/api';
const bgColor = 'transparent';
const borderSize = '5px';
const verticalPadding = '0.8em';
const Container = styled.div`
flex: 0.5;
display: flex;
flex-direction: column;
`;
const MenuItem = styled.div<{ selected: boolean }>`
padding: ${verticalPadding} 0 ${verticalPadding} 1em;
height: 1.5em;
display: flex;
align-items: center;
cursor: pointer;
background-color: ${props =>
props.selected ? 'var(--blue_trans1)' : bgColor};
border-left: ${`${borderSize} solid ${bgColor}`};
border-left-color: ${props => (props.selected ? 'var(--blue)' : bgColor)};
:first-child {
margin-top: ${props => (props.selected ? '0' : `-${borderSize}`)};
}
:hover {
background-color: ${props =>
props.selected ? 'var(--blue_trans1)' : 'var(--blue_trans2)'};
border-left: ${`${borderSize} solid ${bgColor}`};
border-left-color: ${props =>
props.selected ? 'var(--blue)' : 'transparent'};
:first-child {
margin-top: ${props => (props.selected ? '0' : `-${borderSize}`)};
padding-top: ${props =>
props.selected
? `${verticalPadding}`
: `calc(${verticalPadding} - ${borderSize})`};
border-top: ${props =>
props.selected ? '' : `${borderSize} solid var(--blue_opaque1)`};
}
}
`;
const MenuContainer = styled.div`
display: flex;
flex-direction: column;
overflow-y: auto;
height: var(--content_height);
box-sizing: border-box;
`;
const Header = styled.div<{ firstChildSelected: boolean }>`
border-bottom: ${props =>
props.firstChildSelected ? '' : '5px solid var(--blue_trans1)'};
min-height: 2.5em;
padding: 0 10px;
display: flex;
align-items: center;
`;
export const Examples = () => {
const examples = useSelector<AppState, ExamplesState['list']>(
(state: AppState) => state.examples.list
);
const selectedExample = useSelector<AppState, ExamplesState['selected']>(
(state: AppState) => state.examples.selected
);
const dispatch = useDispatch();
return (
<Container>
<Header
firstChildSelected={
!!selectedExample && examples[0].id === selectedExample.id
}
>
<span>Examples</span>
</Header>
<MenuContainer>
{examples.map(example => {
return (
<MenuItem
id={example.id}
key={example.id}
selected={!!selectedExample && example.id === selectedExample.id}
onClick={async () => {
const response = await getExample(example.id);
dispatch({ ...new ChangeSelectedAction(response) });
}}
>
{example.name}
</MenuItem>
);
})}
</MenuContainer>
</Container>
);
};

View File

@ -0,0 +1,83 @@
import React, { useState } from 'react';
import styled, { css } from 'styled-components';
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
`;
const Button = styled.a`
margin: 0.1em;
width: 1.5em;
height: 1.5em;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5em;
font-weight: bolder;
text-decoration: none;
color: rgba(255, 255, 255, 0.85);
background-color: var(--button_float);
box-shadow: 1px 3px 15px 0px rgba(153, 153, 153, 0.4);
cursor: pointer;
user-select: none;
transform-origin: center center;
transition: all 0.2s ease;
&:hover {
box-shadow: var(--box-shadow);
background-color: var(--blue);
color: rgb(255, 255, 255);
transform: scale(1.2);
}
`;
const Tooltip = styled.div<{ visible?: boolean }>`
position: absolute;
pointer-events: none;
z-index: 3;
white-space: nowrap;
transform: translateX(-6.5em);
font-size: var(--font_sub_size);
color: var(--tooltip_foreground);
background-color: var(--tooltip_background);
border-radius: 6px;
padding: 5px 10px;
opacity: 0;
transition: opacity 0.2s ease 0.2s;
${props =>
props.visible &&
css`
opacity: 1;
`}
`;
export const FloatButtonComponent = (props: {
tooltip: string;
text: string;
href: string;
className?: string;
}) => {
const [isTooltipShowing, setShowTooltip] = useState(false);
return (
<Container className={props.className}>
<Tooltip visible={isTooltipShowing}>{props.tooltip}</Tooltip>
<Button
onMouseOver={() => setShowTooltip(true)}
onMouseOut={() => setShowTooltip(false)}
href={props.href}
target="_blank"
rel="noopener noreferrer"
>
{props.text}
</Button>
</Container>
);
};

View File

@ -0,0 +1,68 @@
import React from 'react';
import styled, { css } from 'styled-components';
const Container = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5em 1em;
font-family: 'DM Sans', 'Open Sans', sans-serif;
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.3);
`;
const Group = styled.div`
display: flex;
align-items: center;
`;
const Logo = styled.div`
font-size: 1.25em;
`;
const Link = styled.a`
text-decoration: none;
color: black;
padding: 0.5em 1em;
&:hover {
color: #3aa0ff;
}
${(props: { versionStyle?: boolean }) =>
props.versionStyle &&
css`
background-color: #efefef;
font-weight: 600;
margin-left: 3em;
&:hover {
color: black;
}
`}
`;
export const HeaderComponent = () => {
return (
<Container>
<Group>
<Link href="https://ligolang.org">
<Logo>LIGO</Logo>
</Link>
<Link versionStyle href="https://ligolang.org/versions">
next
</Link>
</Group>
<Group>
<Link href="https://ligolang.org/docs/intro/installation">Docs</Link>
<Link href="https://ligolang.org/docs/tutorials/get-started/tezos-taco-shop-smart-contract">
Tutorials
</Link>
<Link href="https://ligolang.org/blog">Blog</Link>
<Link href="https://ligolang.org/docs/contributors/origin">
Contribute
</Link>
</Group>
</Container>
);
};

View File

@ -0,0 +1,47 @@
import styled from 'styled-components';
export const Group = styled.div`
display: flex;
flex-direction: column;
`;
export const HGroup = styled.div`
display: flex;
align-items: center;
`;
export const Label = styled.label`
font-size: 1em;
color: rgba(153, 153, 153, 1);
`;
export const Input = styled.input`
margin: 0.3em 0 0.7em 0;
background-color: #eff7ff;
border-style: none;
border-bottom: 5px solid #e1f1ff;
padding: 0.5em;
font-size: 1em;
font-family: Menlo, Monaco, 'Courier New', monospace;
outline: none;
&:focus {
background-color: #e1f1ff;
}
`;
export const Textarea = styled.textarea`
resize: vertical;
margin: 0.3em 0 0.7em 0;
background-color: #eff7ff;
border-style: none;
border-bottom: 5px solid #e1f1ff;
padding: 0.5em;
font-size: 1em;
font-family: Menlo, Monaco, 'Courier New', monospace;
outline: none;
&:focus {
background-color: #e1f1ff;
}
`;

View File

@ -0,0 +1,86 @@
import * as monaco from 'monaco-editor';
import React, { useEffect, useRef } from 'react';
import { useDispatch, useStore } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeCodeAction } from '../redux/editor';
const Container = styled.div`
height: var(--content_height);
/* This font size is used to calcuate code font size */
font-size: 0.8em;
`;
export const MonacoComponent = () => {
let containerRef = useRef(null);
const store = useStore();
const dispatch = useDispatch();
useEffect(() => {
const cleanupFunc: Array<() => void> = [];
const { editor: editorState } = store.getState();
const model = monaco.editor.createModel(
editorState.code,
editorState.language
);
monaco.editor.defineTheme('ligoTheme', {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.background': '#eff7ff',
'editor.lineHighlightBackground': '#cee3ff',
'editorLineNumber.foreground': '#888'
}
});
monaco.editor.setTheme('ligoTheme');
const htmlElement = (containerRef.current as unknown) as HTMLElement;
const fontSize = window
.getComputedStyle(htmlElement, null)
.getPropertyValue('font-size');
const editor = monaco.editor.create(htmlElement, {
fontSize: parseFloat(fontSize),
model: model,
automaticLayout: true,
minimap: {
enabled: false
}
});
const { dispose } = editor.onDidChangeModelContent(() => {
dispatch({ ...new ChangeCodeAction(editor.getValue()) });
});
cleanupFunc.push(dispose);
cleanupFunc.push(
store.subscribe(() => {
const { editor: editorState }: AppState = store.getState();
if (editorState.code !== editor.getValue()) {
editor.setValue(editorState.code);
}
if (editorState.language !== model.getModeId()) {
if (editorState.language === 'reasonligo') {
monaco.editor.setModelLanguage(model, 'javascript');
} else {
monaco.editor.setModelLanguage(model, editorState.language);
}
}
})
);
return function cleanUp() {
cleanupFunc.forEach(f => f());
};
}, [store, dispatch]);
return <Container id="editor" ref={containerRef}></Container>;
};

View File

@ -0,0 +1,149 @@
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { PushSpinner } from 'react-spinners-kit';
import styled, { css } from 'styled-components';
import { AppState } from '../redux/app';
import { CommandState } from '../redux/command';
import { DoneLoadingAction, LoadingState } from '../redux/loading';
import { ResultState } from '../redux/result';
const Container = styled.div<{ visible?: boolean }>`
position: absolute;
box-sizing: border-box;
width: 100%;
height: 100%;
font-family: Menlo, Monaco, 'Courier New', monospace;
overflow: scroll;
display: flex;
transform: translateX(100%);
transition: transform 0.2s ease-in;
${props =>
props.visible &&
css`
transform: translateX(0px);
`}
`;
const CancelButton = styled.div`
display: flex;
justify-content: center;
align-items: center;
color: white;
background-color: #fc683a;
cursor: pointer;
user-select: none;
margin: 1em;
padding: 0.5em 1em;
`;
const Output = styled.div`
flex: 1;
padding: 0.8em;
display: flex;
/* This font size is used to calcuate spinner size */
font-size: 1em;
`;
const LoadingContainer = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const LoadingMessage = styled.div`
padding: 1em 0;
`;
const Pre = styled.pre`
margin: 0;
`;
export const OutputTabComponent = (props: {
selected?: boolean;
onCancel?: () => void;
}) => {
const output = useSelector<AppState, ResultState['output']>(
state => state.result.output
);
const contract = useSelector<AppState, ResultState['contract']>(
state => state.result.contract
);
const loading = useSelector<AppState, LoadingState>(state => state.loading);
const dispatchedAction = useSelector<
AppState,
CommandState['dispatchedAction']
>(state => state.command.dispatchedAction);
const dispatch = useDispatch();
const outputRef = useRef(null);
const [spinnerSize, setSpinnerSize] = useState(50);
useEffect(() => {
const htmlElement = (outputRef.current as unknown) as HTMLElement;
const fontSize = window
.getComputedStyle(htmlElement, null)
.getPropertyValue('font-size');
setSpinnerSize(parseFloat(fontSize) * 3);
}, [setSpinnerSize]);
return (
<Container visible={props.selected}>
<Output id="output" ref={outputRef}>
{loading.loading && (
<LoadingContainer>
<PushSpinner size={spinnerSize} color="#fedace" />
<LoadingMessage>{loading.message}</LoadingMessage>
<CancelButton
onClick={() => {
if (dispatchedAction) {
dispatchedAction.cancel();
}
dispatch({ ...new DoneLoadingAction() });
if (props.onCancel) {
props.onCancel();
}
}}
>
Cancel
</CancelButton>
</LoadingContainer>
)}
{!loading.loading &&
((output.length !== 0 && <Pre>{output}</Pre>) ||
(contract.length !== 0 && (
<span>
The contract was successfully deployed to the babylonnet test
network.
<br />
<br />
The address of your new contract is: <i>{contract}</i>
<br />
<br />
View your new contract using{' '}
<a
target="_blank"
rel="noopener noreferrer"
href={`https://better-call.dev/babylon/${contract}`}
>
Better Call Dev
</a>
!
</span>
)))}
</Output>
</Container>
);
};

View File

@ -0,0 +1,200 @@
import { faCopy } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import styled, { css } from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeShareLinkAction, ShareState } from '../redux/share';
import { share } from '../services/api';
const Container = styled.div`
display: flex;
justify-content: flex-end;
align-items: center;
`;
const Button = styled.div<{ clicked?: boolean }>`
cursor: pointer;
user-select: none;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
height: 2em;
width: 6em;
color: var(--blue);
background-color: white;
border-radius: 1em;
transition: width 0.3s ease-in;
${props =>
props.clicked &&
css`
width: 2em;
background-color: white;
`}
&:hover {
background-color: var(--blue_opaque1);
}
`;
const Label = styled.span<{ visible?: boolean }>`
pointer-events: none;
opacity: 1;
transition: opacity 0.3s ease-in;
${props =>
!props.visible &&
css`
opacity: 0;
`}
`;
const CopyIcon = ({ visible, ...props }: { visible: boolean }) => (
<FontAwesomeIcon {...props} icon={faCopy}></FontAwesomeIcon>
);
const Copy = styled(CopyIcon)`
position: absolute;
pointer-events: none;
opacity: 1;
transition: opacity 0.3s ease-in;
${props =>
!props.visible &&
css`
opacity: 0;
`}
`;
const Input = styled.input<{ visible?: boolean }>`
position: absolute;
background-color: var(--blue);
border-radius: 1em;
opacity: 0;
height: 2em;
width: 2em;
transform: translateX(-0.3em);
border: none;
padding: 0 1em;
font-size: 1em;
color: white;
transition: width 0.3s ease-in;
outline: none;
${props =>
props.visible &&
css`
opacity: 1;
width: 25em;
`}
`;
const Tooltip = styled.div<{ visible?: boolean }>`
position: absolute;
pointer-events: none;
z-index: 3;
transform: translateY(2.5em);
font-size: var(--font_sub_size);
color: var(--tooltip_foreground);
background-color: var(--tooltip_background);
border-radius: 6px;
padding: 5px 10px;
opacity: 0;
transition: opacity 0.2s ease 0.2s;
${props =>
props.visible &&
css`
opacity: 1;
`}
`;
const shareAction = () => {
return async function(dispatch: Dispatch, getState: () => AppState) {
try {
const { hash } = await share(getState());
dispatch({ ...new ChangeShareLinkAction(hash) });
} catch (ex) {}
};
};
function copy(element: HTMLInputElement): boolean {
element.select();
element.setSelectionRange(0, 99999);
return document.execCommand('copy');
}
export const ShareComponent = () => {
const inputEl = useRef<HTMLInputElement>(null);
const dispatch = useDispatch();
const shareLink = useSelector<AppState, ShareState['link']>(
state => state.share.link
);
const [clicked, setClicked] = useState(false);
const [isTooltipShowing, setShowTooltip] = useState(false);
const SHARE_TOOLTIP = 'Share code';
const COPY_TOOLTIP = 'Copy link';
const COPIED_TOOLTIP = 'Copied!';
const [tooltipMessage, setTooltipMessage] = useState(SHARE_TOOLTIP);
useEffect(() => {
if (shareLink) {
if (inputEl.current && copy(inputEl.current)) {
setTooltipMessage(COPIED_TOOLTIP);
setShowTooltip(true);
} else {
setClicked(true);
setTooltipMessage(COPY_TOOLTIP);
}
} else {
setClicked(false);
setShowTooltip(false);
setTooltipMessage(SHARE_TOOLTIP);
}
}, [shareLink]);
return (
<Container>
<Input
id="share-link"
visible={!!shareLink}
readOnly
ref={inputEl}
value={shareLink ? `${window.location.origin}/p/${shareLink}` : ''}
></Input>
<Button
id="share"
clicked={clicked}
onMouseOver={() => {
if (tooltipMessage === COPIED_TOOLTIP) {
setTooltipMessage(COPY_TOOLTIP);
}
setShowTooltip(true);
}}
onMouseOut={() => setShowTooltip(false)}
onClick={() => {
if (!shareLink) {
dispatch(shareAction());
setClicked(true);
setTooltipMessage(COPY_TOOLTIP);
} else if (inputEl.current) {
copy(inputEl.current);
setTooltipMessage(COPIED_TOOLTIP);
}
}}
>
<Label visible={!clicked}>Share</Label>
<Copy visible={clicked}></Copy>
<Tooltip visible={isTooltipShowing}>{tooltipMessage}</Tooltip>
</Button>
</Container>
);
};

View File

@ -0,0 +1,148 @@
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';
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;
align-items: center;
justify-content: space-between;
height: 2em;
padding: 0 0.5em;
border: 1px solid var(--blue_trans1);
`;
const ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => (
<FontAwesomeIcon {...props} icon={faCaretDown} size="lg"></FontAwesomeIcon>
);
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<AppState, EditorState['language']>(
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 (
<Container>
<OutsideClickHandler onOutsideClick={() => open(false)}>
<List visible={opened}>
{moveOptionToTop(language).map(option => (
<Option
id={option}
key={option}
onClick={() => selectOption(option)}
>
<span>{OPTIONS[option]}</span>
</Option>
))}
</List>
</OutsideClickHandler>
<Header id="syntax-select" onClick={() => open(true)}>
<span>{OPTIONS[language]}</span>
<Arrow rotate={opened}></Arrow>
</Header>
</Container>
);
};

View File

@ -0,0 +1,94 @@
import React, { useState } from 'react';
import styled, { css } from 'styled-components';
import { ConfigureTabComponent } from './configure-tab';
import { OutputTabComponent } from './output-tab';
const Container = styled.div`
flex: 1;
display: flex;
flex-direction: column;
`;
const Header = styled.div`
display: flex;
border-bottom: 5px solid var(--blue_trans1);
min-height: 2.5em;
`;
const Label = styled.span`
cursor: pointer;
user-select: none;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
&:hover {
color: var(--orange);
}
`;
const Tab = styled.div<{ selected?: boolean }>`
flex: 1;
display: flex;
flex-direction: column;
`;
const Underline = styled.div<{ selectedTab: number }>`
position: relative;
top: -5px;
background-color: var(--orange);
height: 5px;
margin-bottom: -5px;
width: calc(100% / 2);
transition: transform 0.2s ease-in;
${props =>
css`
transform: translateX(calc(${props.selectedTab} * 100%));
`}
`;
const Content = styled.div`
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
`;
export const TabsPanelComponent = () => {
const TABS = [
{ index: 0, label: 'Configure', id: 'configure-tab' },
{ index: 1, label: 'Output', id: 'output-tab' }
];
const [selectedTab, selectTab] = useState(TABS[0]);
return (
<Container>
<Header>
{TABS.map(tab => (
<Tab id={tab.id} selected={selectedTab.index === tab.index}>
<Label onClick={() => selectTab(tab)}>{tab.label}</Label>
</Tab>
))}
</Header>
<Underline selectedTab={selectedTab.index}></Underline>
<Content>
<ConfigureTabComponent
selected={selectedTab.index === 0}
onRun={() => {
selectTab(TABS[1]);
}}
></ConfigureTabComponent>
<OutputTabComponent
selected={selectedTab.index === 1}
onCancel={() => {
selectTab(TABS[0]);
}}
></OutputTabComponent>
</Content>
</Container>
);
};

View File

@ -0,0 +1,83 @@
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 }) => (
<FontAwesomeIcon {...props} icon={faCheck}></FontAwesomeIcon>
);
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 (
<Container className={props.className} checked={isChecked}>
<Button
checked={isChecked}
onClick={() => {
const newState = !isChecked;
setChecked(newState);
props.onChanged(newState);
}}
>
<Check visible={isChecked}></Check>
</Button>
</Container>
);
};

View File

@ -0,0 +1,31 @@
import { applyMiddleware, createStore, Middleware } from 'redux';
import ReduxThunk from 'redux-thunk';
import rootReducer, { AppState } from './redux/app';
declare var defaultServerState: AppState | undefined;
export default function configureStore() {
const store = createStore(
rootReducer,
{
...(typeof defaultServerState === 'undefined' ? {} : defaultServerState)
},
applyMiddleware(ReduxThunk, cleanRouteOnAction)
);
return store;
}
const cleanRouteOnAction: Middleware = store => next => action => {
const { share } = store.getState();
next(action);
const state = store.getState();
if (
share.link !== undefined &&
state.share.link === undefined &&
window.location.pathname !== '/'
) {
window.history.replaceState({}, document.title, '/');
}
};

View File

@ -0,0 +1,79 @@
:root {
/* Note: the LIGO header should be ripped from the main ligolang.org homepage. Specs not included here :-) */
/* width of all colored bands: 4px */
--orange: #fc683a;
--orange_trans: #fedace;
--blue: rgba(14, 116, 255, 1);
--button_float: rgba(14, 116, 255, 0.85);
--blue_trans1: rgba(14, 116, 255, 0.15); /* #e1f1ff; */
--blue_opaque1: #dbeaff;
--blue_trans2: rgba(14, 116, 255, 0.08); /* #eff7ff; */
--grey: #888;
--box-shadow: 1px 3px 10px 0px rgba(153, 153, 153, 0.4); /* or #999999 */
--border_radius: 3px;
/* text, where 1rem = 16px */
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
--font_weight: 400;
--font_menu_hover: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
--font_menu_size: 1rem;
--font_menu_color: rgba(0, 0, 0, 1);
--font_sub_size: 0.8em;
--font_sub_color: rgba(51, 51, 51, 1); /* or #333333; */
--font_label: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
--font_label_size: 0.8rem;
--font_label_color: rgba(153, 153, 153, 1); /* or #999999 */
--font_code: Consolas, source-code-pro, Menlo, Monaco, 'Courier New',
monospace;
--font_code_size: 0.8rem;
--font_code_color: rgba(51, 51, 51, 1); /* or #333333; */
/* filler text for empty panel */
--font_ghost: 2rem;
--font_ghost_weight: 700;
--font_ghost_color: rgba(153, 153, 153, 0.5); /* or #CFCFCF */
--content_height: 85vh;
--tooltip_foreground: white;
--tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/;
}
body {
margin: 0;
font-size: 1.1vw;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.monaco-editor .current-line ~ .line-numbers {
color: var(--orange);
border-left: 4px solid var(--blue);
}
.monaco-editor .margin-view-overlays .current-line,
.monaco-editor .view-overlays .current-line {
background-color: var(--blue_trans1);
color: var(--blue);
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,13 @@
export abstract class CancellableAction {
private cancelled = false;
cancel() {
this.cancelled = true;
}
isCancelled() {
return this.cancelled;
}
abstract getAction(): any;
}

View File

@ -0,0 +1,39 @@
import { Dispatch } from 'redux';
import { compileContract, getErrorMessage } from '../../services/api';
import { AppState } from '../app';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { CancellableAction } from './cancellable';
export class CompileAction extends CancellableAction {
getAction() {
return async (dispatch: Dispatch, getState: () => AppState) => {
dispatch({ ...new UpdateLoadingAction('Compiling contract...') });
try {
const { editor, compile: compileState } = getState();
const michelsonCode = await compileContract(
editor.language,
editor.code,
compileState.entrypoint
);
if (this.isCancelled()) {
return;
}
dispatch({ ...new ChangeOutputAction(michelsonCode.result) });
} catch (ex) {
if (this.isCancelled()) {
return;
}
dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
});
}
dispatch({ ...new DoneLoadingAction() });
};
}
}

View File

@ -0,0 +1,99 @@
import { Tezos } from '@taquito/taquito';
import { TezBridgeSigner } from '@taquito/tezbridge-signer';
import { Dispatch } from 'redux';
import { compileContract, compileExpression, deploy, getErrorMessage } from '../../services/api';
import { AppState } from '../app';
import { MichelsonFormat } from '../compile';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeContractAction, ChangeOutputAction } from '../result';
import { CancellableAction } from './cancellable';
Tezos.setProvider({
rpc: 'https://api.tez.ie/rpc/babylonnet',
signer: new TezBridgeSigner()
});
export class DeployAction extends CancellableAction {
async deployWithTezBridge(dispatch: Dispatch, getState: () => AppState) {
dispatch({ ...new UpdateLoadingAction('Compiling contract...') });
const { editor: editorState, deploy: deployState } = getState();
const michelsonCode = await compileContract(
editorState.language,
editorState.code,
deployState.entrypoint,
MichelsonFormat.Json
);
if (this.isCancelled()) {
return;
}
dispatch({ ...new UpdateLoadingAction('Compiling storage...') });
const michelsonStorage = await compileExpression(
editorState.language,
deployState.storage,
MichelsonFormat.Json
);
if (this.isCancelled()) {
return;
}
dispatch({ ...new UpdateLoadingAction('Waiting for TezBridge signer...') });
const op = await Tezos.contract.originate({
code: JSON.parse(michelsonCode.result),
init: JSON.parse(michelsonStorage.result)
});
if (this.isCancelled()) {
return;
}
dispatch({ ...new UpdateLoadingAction('Deploying to babylon network...') });
return await op.contract();
}
async deployOnServerSide(dispatch: Dispatch, getState: () => AppState) {
dispatch({ ...new UpdateLoadingAction('Deploying to babylon network...') });
const { editor: editorState, deploy: deployState } = getState();
return await deploy(
editorState.language,
editorState.code,
deployState.entrypoint,
deployState.storage
);
}
getAction() {
return async (dispatch: Dispatch, getState: () => AppState) => {
const { deploy } = getState();
try {
const contract = deploy.useTezBridge
? await this.deployWithTezBridge(dispatch, getState)
: await this.deployOnServerSide(dispatch, getState);
if (!contract || this.isCancelled()) {
return;
}
dispatch({ ...new ChangeContractAction(contract.address) });
} catch (ex) {
if (this.isCancelled()) {
return;
}
dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
});
}
dispatch({ ...new DoneLoadingAction() });
};
}
}

View File

@ -0,0 +1,41 @@
import { Dispatch } from 'redux';
import { dryRun, getErrorMessage } from '../../services/api';
import { AppState } from '../app';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { CancellableAction } from './cancellable';
export class DryRunAction extends CancellableAction {
getAction() {
return async (dispatch: Dispatch, getState: () => AppState) => {
dispatch({
...new UpdateLoadingAction('Waiting for dry run results...')
});
try {
const { editor, dryRun: dryRunState } = getState();
const result = await dryRun(
editor.language,
editor.code,
dryRunState.entrypoint,
dryRunState.parameters,
dryRunState.storage
);
if (this.isCancelled()) {
return;
}
dispatch({ ...new ChangeOutputAction(result.output) });
} catch (ex) {
if (this.isCancelled()) {
return;
}
dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
});
}
dispatch({ ...new DoneLoadingAction() });
};
}
}

View File

@ -0,0 +1,43 @@
import { Dispatch } from 'redux';
import { getErrorMessage, runFunction } from '../../services/api';
import { AppState } from '../app';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { CancellableAction } from './cancellable';
export class EvaluateFunctionAction extends CancellableAction {
getAction() {
return async (dispatch: Dispatch, getState: () => AppState) => {
const { editor, evaluateFunction: evaluateFunctionState } = getState();
dispatch({
...new UpdateLoadingAction(
`Evaluating ${evaluateFunctionState.entrypoint} ${evaluateFunctionState.parameters}...`
)
});
try {
const result = await runFunction(
editor.language,
editor.code,
evaluateFunctionState.entrypoint,
evaluateFunctionState.parameters
);
if (this.isCancelled()) {
return;
}
dispatch({ ...new ChangeOutputAction(result.output) });
} catch (ex) {
if (this.isCancelled()) {
return;
}
dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
});
}
dispatch({ ...new DoneLoadingAction() });
};
}
}

View File

@ -0,0 +1,44 @@
import { Dispatch } from 'redux';
import { evaluateValue, getErrorMessage } from '../../services/api';
import { AppState } from '../app';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { CancellableAction } from './cancellable';
export class EvaluateValueAction extends CancellableAction {
getAction() {
return async (dispatch: Dispatch, getState: () => AppState) => {
const { editor, evaluateValue: evaluateValueState } = getState();
dispatch({
...new UpdateLoadingAction(
`Evaluating "${evaluateValueState.entrypoint}" entrypoint...`
)
});
try {
const result = await evaluateValue(
editor.language,
editor.code,
evaluateValueState.entrypoint
);
if (this.isCancelled()) {
return;
}
dispatch({ ...new ChangeOutputAction(result.code) });
} catch (ex) {
if (this.isCancelled()) {
return;
}
dispatch({
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
});
}
dispatch({ ...new DoneLoadingAction() });
};
}
}

View File

@ -0,0 +1,41 @@
import { combineReducers } from 'redux';
import command, { CommandState } from './command';
import compile, { CompileState } from './compile';
import deploy, { DeployState } from './deploy';
import dryRun, { DryRunState } from './dry-run';
import editor, { EditorState } from './editor';
import evaluateFunction, { EvaluateFunctionState } from './evaluate-function';
import evaluateValue, { EvaluateValueState } from './evaluate-value';
import examples, { ExamplesState } from './examples';
import loading, { LoadingState } from './loading';
import result, { ResultState } from './result';
import share, { ShareState } from './share';
export interface AppState {
editor: EditorState;
share: ShareState;
compile: CompileState;
dryRun: DryRunState;
deploy: DeployState;
evaluateFunction: EvaluateFunctionState;
evaluateValue: EvaluateValueState;
result: ResultState;
command: CommandState;
examples: ExamplesState;
loading: LoadingState;
}
export default combineReducers({
editor,
share,
compile,
dryRun,
deploy,
evaluateFunction,
evaluateValue,
result,
command,
examples,
loading
});

View File

@ -0,0 +1,45 @@
import { CancellableAction } from './actions/cancellable';
import { Command } from './types';
export enum ActionType {
ChangeSelected = 'command-change-selected',
ChangeDispatchedAction = 'command-change-dispatched-action'
}
export interface CommandState {
selected: Command;
dispatchedAction: CancellableAction | null;
}
export class ChangeSelectedAction {
public readonly type = ActionType.ChangeSelected;
constructor(public payload: CommandState['selected']) {}
}
export class ChangeDispatchedAction {
public readonly type = ActionType.ChangeDispatchedAction;
constructor(public payload: CommandState['dispatchedAction']) {}
}
type Action = ChangeSelectedAction | ChangeDispatchedAction;
const DEFAULT_STATE: CommandState = {
selected: Command.Compile,
dispatchedAction: null
};
export default (state = DEFAULT_STATE, action: Action): CommandState => {
switch (action.type) {
case ActionType.ChangeSelected:
return {
...state,
selected: action.payload
};
case ActionType.ChangeDispatchedAction:
return {
...state,
dispatchedAction: action.payload
};
}
return state;
};

View File

@ -0,0 +1,41 @@
import { ActionType as ExamplesActionType, ChangeSelectedAction as ChangeSelectedExampleAction } from './examples';
export enum MichelsonFormat {
Text = 'text',
Json = 'json'
}
export enum ActionType {
ChangeEntrypoint = 'compile-change-entrypoint'
}
export interface CompileState {
entrypoint: string;
}
export class ChangeEntrypointAction {
public readonly type = ActionType.ChangeEntrypoint;
constructor(public payload: CompileState['entrypoint']) {}
}
type Action = ChangeEntrypointAction | ChangeSelectedExampleAction;
const DEFAULT_STATE: CompileState = {
entrypoint: ''
};
export default (state = DEFAULT_STATE, action: Action): CompileState => {
switch (action.type) {
case ExamplesActionType.ChangeSelected:
return {
...state,
...(!action.payload ? DEFAULT_STATE : action.payload.compile)
};
case ActionType.ChangeEntrypoint:
return {
...state,
entrypoint: action.payload
};
}
return state;
};

View File

@ -0,0 +1,66 @@
import { ActionType as ExamplesActionType, ChangeSelectedAction as ChangeSelectedExampleAction } from './examples';
export enum ActionType {
ChangeEntrypoint = 'deploy-change-entrypoint',
ChangeStorage = 'deploy-change-storage',
UseTezBridge = 'deploy-use-tezbridge'
}
export interface DeployState {
entrypoint: string;
storage: string;
useTezBridge: boolean;
}
export class ChangeEntrypointAction {
public readonly type = ActionType.ChangeEntrypoint;
constructor(public payload: DeployState['entrypoint']) {}
}
export class ChangeStorageAction {
public readonly type = ActionType.ChangeStorage;
constructor(public payload: DeployState['storage']) {}
}
export class UseTezBridgeAction {
public readonly type = ActionType.UseTezBridge;
constructor(public payload: DeployState['useTezBridge']) {}
}
type Action =
| ChangeEntrypointAction
| ChangeStorageAction
| UseTezBridgeAction
| ChangeSelectedExampleAction;
const DEFAULT_STATE: DeployState = {
entrypoint: '',
storage: '',
useTezBridge: false
};
export default (state = DEFAULT_STATE, action: Action): DeployState => {
switch (action.type) {
case ExamplesActionType.ChangeSelected:
return {
...state,
...(!action.payload ? DEFAULT_STATE : action.payload.deploy)
};
case ActionType.ChangeEntrypoint:
return {
...state,
entrypoint: action.payload
};
case ActionType.ChangeStorage:
return {
...state,
storage: action.payload
};
case ActionType.UseTezBridge:
return {
...state,
useTezBridge: action.payload
};
}
return state;
};

View File

@ -0,0 +1,66 @@
import { ActionType as ExamplesActionType, ChangeSelectedAction as ChangeSelectedExampleAction } from './examples';
export enum ActionType {
ChangeEntrypoint = 'dry-run-change-entrypoint',
ChangeParameters = 'dry-run-change-parameters',
ChangeStorage = 'dry-run-change-storage'
}
export interface DryRunState {
entrypoint: string;
parameters: string;
storage: string;
}
export class ChangeEntrypointAction {
public readonly type = ActionType.ChangeEntrypoint;
constructor(public payload: DryRunState['entrypoint']) {}
}
export class ChangeParametersAction {
public readonly type = ActionType.ChangeParameters;
constructor(public payload: DryRunState['parameters']) {}
}
export class ChangeStorageAction {
public readonly type = ActionType.ChangeStorage;
constructor(public payload: DryRunState['storage']) {}
}
type Action =
| ChangeEntrypointAction
| ChangeParametersAction
| ChangeStorageAction
| ChangeSelectedExampleAction;
const DEFAULT_STATE: DryRunState = {
entrypoint: '',
parameters: '',
storage: ''
};
export default (state = DEFAULT_STATE, action: Action): DryRunState => {
switch (action.type) {
case ExamplesActionType.ChangeSelected:
return {
...state,
...(!action.payload ? DEFAULT_STATE : action.payload.dryRun)
};
case ActionType.ChangeEntrypoint:
return {
...state,
entrypoint: action.payload
};
case ActionType.ChangeParameters:
return {
...state,
parameters: action.payload
};
case ActionType.ChangeStorage:
return {
...state,
storage: action.payload
};
}
return state;
};

View File

@ -0,0 +1,53 @@
import { ActionType as ExamplesActionType, ChangeSelectedAction as ChangeSelectedExampleAction } from './examples';
import { Language } from './types';
export enum ActionType {
ChangeLanguage = 'editor-change-language',
ChangeCode = 'editor-change-code'
}
export interface EditorState {
language: Language;
code: string;
}
export class ChangeLanguageAction {
public readonly type = ActionType.ChangeLanguage;
constructor(public payload: EditorState['language']) {}
}
export class ChangeCodeAction {
public readonly type = ActionType.ChangeCode;
constructor(public payload: EditorState['code']) {}
}
type Action =
| ChangeCodeAction
| ChangeLanguageAction
| ChangeSelectedExampleAction;
const DEFAULT_STATE: EditorState = {
language: Language.CameLigo,
code: ''
};
export default (state = DEFAULT_STATE, action: Action): EditorState => {
switch (action.type) {
case ExamplesActionType.ChangeSelected:
return {
...state,
...(!action.payload ? DEFAULT_STATE : action.payload.editor)
};
case ActionType.ChangeLanguage:
return {
...state,
language: action.payload
};
case ActionType.ChangeCode:
return {
...state,
code: action.payload
};
}
return state;
};

View File

@ -0,0 +1,55 @@
import { ActionType as ExamplesActionType, ChangeSelectedAction as ChangeSelectedExampleAction } from './examples';
export enum ActionType {
ChangeEntrypoint = 'evaluate-function-change-entrypoint',
ChangeParameters = 'evaluate-function-change-parameters'
}
export interface EvaluateFunctionState {
entrypoint: string;
parameters: string;
}
export class ChangeEntrypointAction {
public readonly type = ActionType.ChangeEntrypoint;
constructor(public payload: EvaluateFunctionState['entrypoint']) {}
}
export class ChangeParametersAction {
public readonly type = ActionType.ChangeParameters;
constructor(public payload: EvaluateFunctionState['parameters']) {}
}
type Action =
| ChangeEntrypointAction
| ChangeParametersAction
| ChangeSelectedExampleAction;
const DEFAULT_STATE: EvaluateFunctionState = {
entrypoint: '',
parameters: ''
};
export default (
state = DEFAULT_STATE,
action: Action
): EvaluateFunctionState => {
switch (action.type) {
case ExamplesActionType.ChangeSelected:
return {
...state,
...(!action.payload ? DEFAULT_STATE : action.payload.evaluateFunction)
};
case ActionType.ChangeEntrypoint:
return {
...state,
entrypoint: action.payload
};
case ActionType.ChangeParameters:
return {
...state,
parameters: action.payload
};
}
return state;
};

View File

@ -0,0 +1,36 @@
import { ActionType as ExamplesActionType, ChangeSelectedAction as ChangeSelectedExampleAction } from './examples';
export enum ActionType {
ChangeEntrypoint = 'evaluate-value-change-entrypoint'
}
export interface EvaluateValueState {
entrypoint: string;
}
export class ChangeEntrypointAction {
public readonly type = ActionType.ChangeEntrypoint;
constructor(public payload: EvaluateValueState['entrypoint']) {}
}
type Action = ChangeEntrypointAction | ChangeSelectedExampleAction;
const DEFAULT_STATE: EvaluateValueState = {
entrypoint: ''
};
export default (state = DEFAULT_STATE, action: Action): EvaluateValueState => {
switch (action.type) {
case ExamplesActionType.ChangeSelected:
return {
...state,
...(!action.payload ? DEFAULT_STATE : action.payload.evaluateValue)
};
case ActionType.ChangeEntrypoint:
return {
...state,
entrypoint: action.payload
};
}
return state;
};

View File

@ -0,0 +1,17 @@
import { CompileState } from './compile';
import { DeployState } from './deploy';
import { DryRunState } from './dry-run';
import { EditorState } from './editor';
import { EvaluateFunctionState } from './evaluate-function';
import { EvaluateValueState } from './evaluate-value';
export interface ExampleState {
id: string;
name: string;
editor: EditorState;
compile: CompileState;
dryRun: DryRunState;
deploy: DeployState;
evaluateFunction: EvaluateFunctionState;
evaluateValue: EvaluateValueState;
}

View File

@ -0,0 +1,38 @@
import { ExampleState } from './example';
export enum ActionType {
ChangeSelected = 'examples-change-selected'
}
export interface ExampleItem {
id: string;
name: string;
}
export interface ExamplesState {
selected: ExampleState | null;
list: ExampleItem[];
}
export class ChangeSelectedAction {
public readonly type = ActionType.ChangeSelected;
constructor(public payload: ExamplesState['selected']) {}
}
type Action = ChangeSelectedAction;
export const DEFAULT_STATE: ExamplesState = {
selected: null,
list: []
};
export default (state = DEFAULT_STATE, action: Action): ExamplesState => {
switch (action.type) {
case ActionType.ChangeSelected:
return {
...state,
selected: action.payload
};
}
return state;
};

View File

@ -0,0 +1,42 @@
export enum ActionType {
UpdateLoading = 'loading-update-loading',
DoneLoading = 'loading-done-loading'
}
export interface LoadingState {
loading: boolean;
message: string;
}
export class UpdateLoadingAction {
public readonly type = ActionType.UpdateLoading;
constructor(public payload: LoadingState['message']) {}
}
export class DoneLoadingAction {
public readonly type = ActionType.DoneLoading;
}
type Action = UpdateLoadingAction | DoneLoadingAction;
export const DEFAULT_STATE: LoadingState = {
loading: false,
message: ''
};
export default (state = DEFAULT_STATE, action: Action): LoadingState => {
switch (action.type) {
case ActionType.UpdateLoading:
return {
...state,
loading: true,
message: action.payload
};
case ActionType.DoneLoading:
return {
...state,
...DEFAULT_STATE
};
}
return state;
};

View File

@ -0,0 +1,43 @@
export enum ActionType {
ChangeOutput = 'result-change-output',
ChangeContract = 'result-change-contract'
}
export interface ResultState {
output: string;
contract: string;
}
export class ChangeOutputAction {
public readonly type = ActionType.ChangeOutput;
constructor(public payload: ResultState['output']) {}
}
export class ChangeContractAction {
public readonly type = ActionType.ChangeContract;
constructor(public payload: ResultState['contract']) {}
}
type Action = ChangeOutputAction | ChangeContractAction;
const DEFAULT_STATE: ResultState = {
output: '',
contract: ''
};
export default (state = DEFAULT_STATE, action: Action): ResultState => {
switch (action.type) {
case ActionType.ChangeOutput:
return {
...state,
output: action.payload
};
case ActionType.ChangeContract:
return {
...state,
output: DEFAULT_STATE.output,
contract: action.payload
};
}
return state;
};

View File

@ -0,0 +1,82 @@
import { ActionType as CompileActionType, ChangeEntrypointAction as ChangeCompileEntrypointAction } from './compile';
import {
ActionType as DeployActionType,
ChangeEntrypointAction as ChangeDeployEntrypointAction,
ChangeStorageAction as ChangeDeployStorageAction,
UseTezBridgeAction,
} from './deploy';
import {
ActionType as DryRunActionType,
ChangeEntrypointAction as ChangeDryRunEntrypointAction,
ChangeParametersAction as ChangeDryRunParametersAction,
ChangeStorageAction as ChangeDryRunStorageAction,
} from './dry-run';
import { ActionType as EditorActionType, ChangeCodeAction, ChangeLanguageAction } from './editor';
import {
ActionType as EvaluateFunctionActionType,
ChangeEntrypointAction as ChangeEvaluateFunctionEntrypointAction,
ChangeParametersAction as ChangeEvaluateFunctionParametersAction,
} from './evaluate-function';
import {
ActionType as EvaluateValueActionType,
ChangeEntrypointAction as ChangeEvaluateValueEntrypointAction,
} from './evaluate-value';
export enum ActionType {
ChangeShareLink = 'share-change-link'
}
export interface ShareState {
link: string;
}
export class ChangeShareLinkAction {
public readonly type = ActionType.ChangeShareLink;
constructor(public payload: ShareState['link']) {}
}
type Action =
| ChangeShareLinkAction
| ChangeCodeAction
| ChangeLanguageAction
| ChangeCompileEntrypointAction
| ChangeDeployEntrypointAction
| ChangeDeployStorageAction
| UseTezBridgeAction
| ChangeDryRunEntrypointAction
| ChangeDryRunParametersAction
| ChangeDryRunStorageAction
| ChangeEvaluateFunctionEntrypointAction
| ChangeEvaluateFunctionParametersAction
| ChangeEvaluateValueEntrypointAction;
const DEFAULT_STATE: ShareState = {
link: ''
};
export default (state = DEFAULT_STATE, action: Action): ShareState => {
switch (action.type) {
case EditorActionType.ChangeCode:
case EditorActionType.ChangeLanguage:
case CompileActionType.ChangeEntrypoint:
case DeployActionType.ChangeEntrypoint:
case DeployActionType.ChangeStorage:
case DeployActionType.UseTezBridge:
case DryRunActionType.ChangeEntrypoint:
case DryRunActionType.ChangeParameters:
case DryRunActionType.ChangeStorage:
case EvaluateFunctionActionType.ChangeEntrypoint:
case EvaluateFunctionActionType.ChangeParameters:
case EvaluateValueActionType.ChangeEntrypoint:
return {
...state,
...DEFAULT_STATE
};
case ActionType.ChangeShareLink:
return {
...state,
link: action.payload
};
}
return state;
};

View File

@ -0,0 +1,13 @@
export enum Language {
PascaLigo = 'pascaligo',
CameLigo = 'cameligo',
ReasonLIGO = 'reasonligo'
}
export enum Command {
Compile = 'compile',
DryRun = 'dry-run',
EvaluateValue = 'evaluate-value',
EvaluateFunction = 'evaluate-function',
Deploy = 'deploy'
}

View File

@ -0,0 +1,143 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@ -0,0 +1,132 @@
import axios from 'axios';
import { AppState } from '../redux/app';
import { Language } from '../redux/types';
export async function getExample(id: string) {
const response = await axios.get(`/static/examples/${id}`);
return response.data;
}
export async function compileContract(
syntax: Language,
code: string,
entrypoint: string,
format?: string
) {
const response = await axios.post('/api/compile-contract', {
syntax,
code,
entrypoint,
format
});
return response.data;
}
export async function compileExpression(
syntax: Language,
expression: string,
format?: string
) {
const response = await axios.post('/api/compile-expression', {
syntax,
expression,
format
});
return response.data;
}
export async function dryRun(
syntax: Language,
code: string,
entrypoint: string,
parameters: string,
storage: 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/dry-run', {
syntax,
code,
entrypoint,
parameters,
storage
});
return response.data;
}
export async function share({
editor,
compile,
dryRun,
deploy,
evaluateValue,
evaluateFunction
}: Partial<AppState>) {
const response = await axios.post('/api/share', {
editor,
compile,
dryRun,
deploy,
evaluateValue,
evaluateFunction
});
return response.data;
}
export async function deploy(
syntax: Language,
code: string,
entrypoint: string,
storage: 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/deploy', {
syntax,
code,
entrypoint,
storage
});
return response.data;
}
export async function evaluateValue(
syntax: Language,
code: string,
entrypoint: string
) {
const response = await axios.post('/api/evaluate-value', {
syntax,
code,
entrypoint
});
return response.data;
}
export async function runFunction(
syntax: Language,
code: string,
entrypoint: string,
parameters: string
) {
const response = await axios.post('/api/run-function', {
syntax,
code,
entrypoint,
parameters
});
return response.data;
}
export function getErrorMessage(ex: any): string {
if (ex.response && ex.response.data) {
return ex.response.data.error;
} else if (ex instanceof Error) {
return ex.message;
}
return JSON.stringify(ex);
}

View File

@ -0,0 +1,11 @@
const proxy = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
proxy({
target: 'http://localhost:8080',
changeOrigin: true
})
);
};

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["src"]
}

View File

@ -0,0 +1,12 @@
FROM alekzonder/puppeteer:latest as puppeteer
WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json
COPY jest-puppeteer.config.js jest-puppeteer.config.js
COPY test test
RUN npm ci
ENTRYPOINT [ "npm", "run", "test" ]

View File

@ -0,0 +1,18 @@
version: '3'
services:
webide:
image: "${WEBIDE_IMAGE}"
environment:
- DATA_DIR=/tmp
volumes:
- /tmp:/tmp
logging:
driver: none
e2e:
build: .
environment:
- API_HOST=http://webide:8080
volumes:
- /tmp:/tmp
depends_on:
- webide

View File

@ -0,0 +1,13 @@
module.exports = {
launch: {
args: [
'--no-sandbox',
'--disable-setuid-sandbox'
],
defaultViewport: {
width: 1920,
height: 1080
},
headless: true
}
};

4820
tools/webide/packages/e2e/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
{
"name": "e2e",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest --runInBand"
},
"jest": {
"preset": "jest-puppeteer"
},
"author": "",
"license": "ISC",
"dependencies": {
"jest": "^24.9.0",
"jest-puppeteer": "^4.3.0",
"puppeteer": "^1.20.0"
},
"devDependencies": {
"node-fetch": "^2.6.0"
}
}

View File

@ -0,0 +1,112 @@
const fetch = require('node-fetch');
//
// Generic utils
//
exports.sleep = (time) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
exports.clearText = async keyboard => {
await keyboard.down('Shift');
for (let i = 0; i < 100; i++) {
await keyboard.press('ArrowUp');
}
await keyboard.up('Shift');
await keyboard.press('Backspace');
await keyboard.down('Shift');
for (let i = 0; i < 100; i++) {
await keyboard.press('ArrowDown');
}
await keyboard.up('Shift');
await keyboard.press('Backspace');
};
exports.createResponseCallback = (page, url) => {
return new Promise(resolve => {
page.on('response', function callback(response) {
if (response.url() === url) {
resolve(response);
page.removeListener('response', callback);
}
});
});
};
exports.getInnerText = id => {
let element = document.getElementById(id);
return element && element.textContent;
};
exports.getInputValue = id => {
let element = document.getElementById(id);
return element && element.value;
};
//
// Application specific utils
//
exports.API_HOST = process.env['API_HOST'] || 'http://127.0.0.1:8080';
exports.API_ROOT = `${exports.API_HOST}/api`;
exports.fetchExamples = async () => (await fetch(`${exports.API_HOST}/static/examples/list`)).json();
exports.runCommandAndGetOutputFor = async (command, endpoint) => {
await page.click('#configure-tab');
await exports.sleep(1000);
await page.click('#command-select');
await page.click(`#${command}`);
// Gotta create response callback before clicking run because some responses are too fast
const responseCallback = exports.createResponseCallback(page, `${exports.API_ROOT}/${endpoint}`);
await page.click('#run');
await responseCallback;
return page.evaluate(exports.getInnerText, 'output');
};
exports.verifyAllExamples = async (action, done) => {
const examples = await exports.fetchExamples();
for (example of examples) {
await page.click(`#${example.id}`);
expect(await action()).not.toContain('Error: ');
}
done();
};
exports.verifyWithBlankParameter = async (command, parameter, action, done) => {
await page.click('#command-select');
await page.click(`#${command}`);
await page.click(`#${parameter}`);
await exports.clearText(page.keyboard);
expect(await action()).toEqual(`Error: "${parameter}" is not allowed to be empty`);
done();
}
exports.verifyEntrypointBlank = async (command, action, done) => {
exports.verifyWithBlankParameter(command, 'entrypoint', action, done);
}
exports.verifyParametersBlank = async (command, action, done) => {
exports.verifyWithBlankParameter(command, 'parameters', action, done);
}
exports.verifyStorageBlank = async (command, action, done) => {
exports.verifyWithBlankParameter(command, 'storage', action, done);
}
exports.verifyWithCompilationError = async (action, done) => {
await page.click('#editor');
await page.keyboard.type('asdf');
expect(await action()).toContain('Error: ');
done();
};

View File

@ -0,0 +1,34 @@
const commonUtils = require('./common-utils');
const API_HOST = commonUtils.API_HOST;
const runCommandAndGetOutputFor = commonUtils.runCommandAndGetOutputFor;
const verifyEntrypointBlank = commonUtils.verifyEntrypointBlank;
const verifyAllExamples = commonUtils.verifyAllExamples;
const verifyWithCompilationError = commonUtils.verifyWithCompilationError;
const COMMAND = 'compile';
const COMMAND_ENDPOINT = 'compile-contract';
async function action() {
return await runCommandAndGetOutputFor(COMMAND, COMMAND_ENDPOINT);
}
describe('Compile contract', () => {
beforeAll(() => jest.setTimeout(60000));
beforeEach(async () => await page.goto(API_HOST));
it('should compile for each examples', async done => {
verifyAllExamples(action, done);
});
it('should return an error when entrypoint is blank', async done => {
verifyEntrypointBlank(COMMAND, action, done);
});
it('should return an error when code has compilation error', async done => {
verifyWithCompilationError(action, done);
});
});

View File

@ -0,0 +1,44 @@
const commonUtils = require('./common-utils');
const API_HOST = commonUtils.API_HOST;
const runCommandAndGetOutputFor = commonUtils.runCommandAndGetOutputFor;
const verifyAllExamples = commonUtils.verifyAllExamples;
const verifyEntrypointBlank = commonUtils.verifyEntrypointBlank;
const verifyParametersBlank = commonUtils.verifyParametersBlank;
const verifyStorageBlank = commonUtils.verifyStorageBlank;
const verifyWithCompilationError = commonUtils.verifyWithCompilationError;
const COMMAND = 'dry-run';
const COMMAND_ENDPOINT = 'dry-run';
async function action() {
return await runCommandAndGetOutputFor(COMMAND, COMMAND_ENDPOINT);
}
describe('Dry run contract', () => {
beforeAll(() => jest.setTimeout(60000));
beforeEach(async () => await page.goto(API_HOST));
it('should dry run for examples', async done => {
verifyAllExamples(action, done);
});
it('should return an error when entrypoint is blank', async done => {
verifyEntrypointBlank(COMMAND, action, done);
});
it('should return an error when parameters is blank', async done => {
verifyParametersBlank(COMMAND, action, done);
});
it('should return an error when storage is blank', async done => {
verifyStorageBlank(COMMAND, action, done);
});
it('should return an error when code has compilation error', async done => {
verifyWithCompilationError(action, done);
});
});

View File

@ -0,0 +1,39 @@
const commonUtils = require('./common-utils');
const API_HOST = commonUtils.API_HOST;
const runCommandAndGetOutputFor = commonUtils.runCommandAndGetOutputFor;
const verifyAllExamples = commonUtils.verifyAllExamples;
const verifyEntrypointBlank = commonUtils.verifyEntrypointBlank;
const verifyParametersBlank = commonUtils.verifyParametersBlank;
const verifyWithCompilationError = commonUtils.verifyWithCompilationError;
const COMMAND = 'evaluate-function';
const COMMAND_ENDPOINT = 'run-function';
async function action() {
return await runCommandAndGetOutputFor(COMMAND, COMMAND_ENDPOINT);
}
describe('Evaluate function', () => {
beforeAll(() => jest.setTimeout(60000));
beforeEach(async () => await page.goto(API_HOST));
it('should evaluate function for each examples', async done => {
verifyAllExamples(action, done);
});
it('should return an error when entrypoint is blank', async done => {
verifyEntrypointBlank(COMMAND, action, done);
});
it('should return an error when parameters is blank', async done => {
verifyParametersBlank(COMMAND, action, done);
});
it('should return an error when code has compilation error', async done => {
verifyWithCompilationError(action, done);
});
});

View File

@ -0,0 +1,210 @@
const commonUtils = require('./common-utils');
const fs = require('fs');
const API_HOST = commonUtils.API_HOST;
const API_ROOT = commonUtils.API_ROOT;
const getInnerText = commonUtils.getInnerText;
const getInputValue = commonUtils.getInputValue;
const createResponseCallback = commonUtils.createResponseCallback;
const clearText = commonUtils.clearText;
describe('Share', () => {
beforeAll(() => jest.setTimeout(60000));
it('should generate a link', async done => {
await page.goto(API_HOST);
await page.click('#editor');
await clearText(page.keyboard);
await page.keyboard.type('asdf');
const responseCallback = createResponseCallback(page, `${API_ROOT}/share`);
await page.click('#share');
await responseCallback;
const actualShareLink = await page.evaluate(getInputValue, 'share-link');
const expectedShareLink = `${API_HOST}/p/sIpy-2D9ExpCojwuBNw_-g`;
expect(actualShareLink).toEqual(expectedShareLink);
done();
});
it('should work with v0 schema', async done => {
const id = 'v0-schema';
const expectedShareLink = `${API_HOST}/p/${id}`;
const v0State = {
language: 'cameligo',
code: 'somecode',
entrypoint: 'main',
parameters: '1',
storage: '2'
};
fs.writeFileSync(`/tmp/${id}.txt`, JSON.stringify(v0State));
await page.goto(expectedShareLink);
// Check share link is correct
const actualShareLink = await page.evaluate(getInputValue, 'share-link');
expect(actualShareLink).toEqual(expectedShareLink);
// Check the code is correct. Note, because we are getting inner text we will get
// a line number as well. Therefore the expected value has a '1' prefix
const actualCode = await page.evaluate(getInnerText, 'editor');
expect(actualCode).toContain(`1${v0State.code}`);
// Check compile configuration
await page.click('#command-select');
await page.click('#compile');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v0State.entrypoint
);
// Check dry run configuration
await page.click('#command-select');
await page.click('#dry-run');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v0State.entrypoint
);
expect(await page.evaluate(getInputValue, 'parameters')).toEqual(
v0State.parameters
);
expect(await page.evaluate(getInputValue, 'storage')).toEqual(
v0State.storage
);
// Check deploy configuration
await page.click('#command-select');
await page.click('#deploy');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v0State.entrypoint
);
expect(await page.evaluate(getInputValue, 'storage')).toEqual(
v0State.storage
);
// Check evaluate function configuration
await page.click('#command-select');
await page.click('#evaluate-function');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v0State.entrypoint
);
expect(await page.evaluate(getInputValue, 'parameters')).toEqual(
v0State.parameters
);
// Check evaluate value configuration
await page.click('#command-select');
await page.click('#evaluate-value');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v0State.entrypoint
);
done();
});
it('should work with v1 schema', async done => {
const id = 'v1-schema';
const expectedShareLink = `${API_HOST}/p/${id}`;
const v1State = {
version: 'v1',
state: {
editor: {
language: 'cameligo',
code: 'somecode'
},
compile: {
entrypoint: 'main'
},
dryRun: {
entrypoint: 'main',
parameters: '1',
storage: '2'
},
deploy: {
entrypoint: 'main',
storage: '3',
useTezBridge: false
},
evaluateFunction: {
entrypoint: 'add',
parameters: '(1, 2)'
},
evaluateValue: {
entrypoint: 'a'
}
}
};
fs.writeFileSync(`/tmp/${id}.txt`, JSON.stringify(v1State));
await page.goto(expectedShareLink);
// Check share link is correct
const actualShareLink = await page.evaluate(getInputValue, 'share-link');
expect(actualShareLink).toEqual(expectedShareLink);
// Check the code is correct. Note, because we are getting inner text we will get
// a line number as well. Therefore the expected value has a '1' prefix
const actualCode = await page.evaluate(getInnerText, 'editor');
expect(actualCode).toContain(`1${v1State.state.editor.code}`);
// Check compile configuration
await page.click('#command-select');
await page.click('#compile');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v1State.state.compile.entrypoint
);
// Check dry run configuration
await page.click('#command-select');
await page.click('#dry-run');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v1State.state.dryRun.entrypoint
);
expect(await page.evaluate(getInputValue, 'parameters')).toEqual(
v1State.state.dryRun.parameters
);
expect(await page.evaluate(getInputValue, 'storage')).toEqual(
v1State.state.dryRun.storage
);
// Check deploy configuration
await page.click('#command-select');
await page.click('#deploy');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v1State.state.deploy.entrypoint
);
expect(await page.evaluate(getInputValue, 'storage')).toEqual(
v1State.state.deploy.storage
);
// Check evaluate function configuration
await page.click('#command-select');
await page.click('#evaluate-function');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v1State.state.evaluateFunction.entrypoint
);
expect(await page.evaluate(getInputValue, 'parameters')).toEqual(
v1State.state.evaluateFunction.parameters
);
// Check evaluate value configuration
await page.click('#command-select');
await page.click('#evaluate-value');
expect(await page.evaluate(getInputValue, 'entrypoint')).toEqual(
v1State.state.evaluateValue.entrypoint
);
done();
});
});

View File

@ -0,0 +1,22 @@
# Quick Start
```sh
yarn start
open http://localhost:8080
```
# Available Scripts
In the project directory, you can run:
## `yarn start`
Runs the server in development mode. This will also start the client.
## `yarn test`
Runs tests.
## `yarn build`
Builds the application for production to the `dist` folder.

View File

@ -0,0 +1,7 @@
module.exports = {
roots: ['test'],
testMatch: ['**/?(*.)+(spec|test).+(ts|tsx|js)'],
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest'
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"prestart": "cd ../client && npm run build",
"start": "nodemon -r @ts-tools/node/r -r tsconfig-paths/register ./src/index.ts",
"build": "tsc",
"test": "jest"
},
"devDependencies": {
"@ts-tools/node": "^1.0.0",
"@types/express": "^4.17.1",
"@types/express-winston": "^3.0.4",
"@types/hapi__joi": "^16.0.1",
"@types/jest": "^24.0.23",
"@types/joi": "^14.3.3",
"@types/node": "10",
"@types/tmp": "^0.1.0",
"@types/winston": "^2.4.4",
"jest": "^24.9.0",
"nodemon": "^1.19.3",
"ts-jest": "^24.1.0",
"ts-node": "^8.4.1",
"tsconfig-paths": "^3.9.0",
"typescript": "~3.6.3"
},
"author": "",
"license": "ISC",
"dependencies": {
"@google-cloud/storage": "^4.0.0",
"@hapi/joi": "^16.1.7",
"@taquito/taquito": "^5.1.0-beta.1",
"@types/node-fetch": "^2.5.4",
"body-parser": "^1.19.0",
"escape-html": "^1.0.3",
"express": "^4.17.1",
"express-winston": "^4.0.1",
"node-fetch": "^2.6.0",
"sanitize-html": "^1.20.1",
"tmp": "^0.1.0",
"winston": "^3.2.1"
}
}

View File

@ -0,0 +1,49 @@
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;
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(),
format: joi.string().optional()
})
.validate(body);
};
export async function compileContractHandler(req: Request, res: Response) {
const { error, value: body } = validateRequest(req.body);
if (error) {
res.status(400).json({ error: error.message });
} else {
try {
const michelsonCode = await new LigoCompiler().compileContract(
body.syntax,
body.code,
body.entrypoint,
body.format || 'text'
);
res.send({ result: michelsonCode });
} catch (ex) {
if (ex instanceof CompilerError) {
res.status(400).json({ error: ex.message });
} else {
logger.error(ex);
res.sendStatus(500);
}
}
}
}

View File

@ -0,0 +1,46 @@
import joi from '@hapi/joi';
import { Request, Response } from 'express';
import { CompilerError, LigoCompiler } from '../ligo-compiler';
import { logger } from '../logger';
interface CompileBody {
syntax: string;
expression: string;
format?: string;
}
const validateRequest = (body: any): { value: CompileBody; error: any } => {
return joi
.object({
syntax: joi.string().required(),
expression: joi.string().required(),
format: joi.string().optional()
})
.validate(body);
};
export async function compileExpressionHandler(req: Request, res: Response) {
const { error, value: body } = validateRequest(req.body);
if (error) {
res.status(400).json({ error: error.message });
} else {
try {
const michelsonCode = await new LigoCompiler().compileExpression(
body.syntax,
body.expression,
body.format || 'text'
);
res.send({ result: michelsonCode });
} catch (ex) {
if (ex instanceof CompilerError) {
res.status(400).json({ error: ex.message });
} else {
logger.error(ex);
res.sendStatus(500);
}
}
}
}

View File

@ -0,0 +1,68 @@
import joi from '@hapi/joi';
import { Tezos } from '@taquito/taquito';
import { Request, Response } from 'express';
import { CompilerError, LigoCompiler } from '../ligo-compiler';
import { logger } from '../logger';
import { fetchRandomPrivateKey } from '../services/key';
interface DeployBody {
syntax: string;
code: string;
entrypoint: string;
storage: string;
}
Tezos.setProvider({ rpc: 'https://api.tez.ie/rpc/babylonnet' });
const validateRequest = (body: any): { value: DeployBody; error: any } => {
return joi
.object({
syntax: joi.string().required(),
code: joi.string().required(),
entrypoint: joi.string().required(),
storage: joi.string().required()
})
.validate(body);
};
export async function deployHandler(req: Request, res: Response) {
const { error, value: body } = validateRequest(req.body);
if (error) {
res.status(400).json({ error: error.message });
} else {
try {
const michelsonCode = await new LigoCompiler().compileContract(
body.syntax,
body.code,
body.entrypoint,
'json'
);
const michelsonStorage = await new LigoCompiler().compileExpression(
body.syntax,
body.storage,
'json'
);
await Tezos.importKey(await fetchRandomPrivateKey());
const op = await Tezos.contract.originate({
code: JSON.parse(michelsonCode),
init: JSON.parse(michelsonStorage)
});
const contract = await op.contract();
res.send({ ...contract });
} catch (ex) {
if (ex instanceof CompilerError) {
res.status(400).json({ error: ex.message });
} else {
logger.error(ex);
res.sendStatus(500);
}
}
}
}

View File

@ -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 DryRunBody {
syntax: string;
code: string;
entrypoint: string;
parameters: string;
storage: string;
}
const validateRequest = (body: any): { value: DryRunBody; error: any } => {
return joi
.object({
syntax: joi.string().required(),
code: joi.string().required(),
entrypoint: joi.string().required(),
parameters: joi.string().required(),
storage: joi.string().required()
})
.validate(body);
};
export async function dryRunHandler(req: Request, res: Response) {
const { error, value: body } = validateRequest(req.body);
if (error) {
res.status(400).json({ error: error.message });
} else {
try {
const output = await new LigoCompiler().dryRun(
body.syntax,
body.code,
body.entrypoint,
body.parameters,
body.storage
);
res.send({ output: output });
} catch (ex) {
if (ex instanceof CompilerError) {
res.status(400).json({ error: ex.message });
} else {
logger.error(ex);
res.sendStatus(500);
}
}
}
}

View File

@ -0,0 +1,49 @@
import joi from '@hapi/joi';
import { Request, Response } from 'express';
import { CompilerError, LigoCompiler } from '../ligo-compiler';
import { logger } from '../logger';
interface EvaluateValueBody {
syntax: string;
code: string;
entrypoint: string;
}
const validateRequest = (
body: any
): { value: EvaluateValueBody; error: any } => {
return joi
.object({
syntax: joi.string().required(),
code: joi.string().required(),
entrypoint: joi.string().required(),
format: joi.string().optional()
})
.validate(body);
};
export async function evaluateValueHandler(req: Request, res: Response) {
const { error, value: body } = validateRequest(req.body);
if (error) {
res.status(400).json({ error: error.message });
} else {
try {
const michelsonCode = await new LigoCompiler().evaluateValue(
body.syntax,
body.code,
body.entrypoint
);
res.send({ code: michelsonCode });
} catch (ex) {
if (ex instanceof CompilerError) {
res.status(400).json({ error: ex.message });
} else {
logger.error(ex);
res.sendStatus(500);
}
}
}
}

View File

@ -0,0 +1,49 @@
import joi from '@hapi/joi';
import { Request, Response } from 'express';
import { CompilerError, LigoCompiler } from '../ligo-compiler';
import { logger } from '../logger';
interface RunFunctionBody {
syntax: string;
code: string;
entrypoint: string;
parameters: string;
}
const validateRequest = (body: any): { value: RunFunctionBody; error: any } => {
return joi
.object({
syntax: joi.string().required(),
code: joi.string().required(),
entrypoint: joi.string().required(),
parameters: joi.string().required()
})
.validate(body);
};
export async function runFunctionHandler(req: Request, res: Response) {
const { error, value: body } = validateRequest(req.body);
if (error) {
res.status(400).json({ error: error.message });
} else {
try {
const output = await new LigoCompiler().runFunction(
body.syntax,
body.code,
body.entrypoint,
body.parameters
);
res.send({ output: output });
} catch (ex) {
if (ex instanceof CompilerError) {
res.status(400).json({ error: ex.message });
} else {
logger.error(ex);
res.sendStatus(500);
}
}
}
}

View File

@ -0,0 +1,111 @@
import joi from '@hapi/joi';
import { createHash } from 'crypto';
import { Request, Response } from 'express';
import { logger } from '../logger';
import latestSchema from '../schemas/share-latest';
import { storage } from '../storage';
interface ShareBody {
editor: {
language: string;
code: string;
};
compile: {
entrypoint: string;
};
dryRun: {
entrypoint: string;
parameters: string;
storage: string;
};
deploy: {
entrypoint: string;
storage: string;
useTezBridge?: boolean;
};
evaluateValue: {
entrypoint: string;
};
evaluateFunction: {
entrypoint: string;
parameters: string;
};
}
const validateRequest = (body: any): { value: ShareBody; error: any } => {
return joi
.object({
editor: joi
.object({
language: joi.string().required(),
code: joi.string().required()
})
.required(),
compile: joi.object({
entrypoint: joi.string().allow('')
}),
dryRun: joi.object({
entrypoint: joi.string().allow(''),
parameters: joi.any().allow(''),
storage: joi.any().allow('')
}),
deploy: joi.object({
entrypoint: joi.string().allow(''),
storage: joi.any().allow(''),
useTezBridge: joi.boolean().optional()
}),
evaluateValue: joi.object({
entrypoint: joi.string().allow('')
}),
evaluateFunction: joi.object({
entrypoint: joi.string().allow(''),
parameters: joi.any().allow('')
})
})
.validate(body);
};
function escapeUrl(str: string) {
return str
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
export async function shareHandler(req: Request, res: Response) {
const { error, value } = validateRequest(req.body);
if (error) {
res.status(400).json({ error: error.message });
} else {
try {
const versionedShareState = {
version: latestSchema.VERSION,
state: value
};
const { error } = latestSchema.validate(versionedShareState);
if (error) {
logger.error(
`${versionedShareState} doesn't match latest schema ${latestSchema.VERSION}`
);
res.sendStatus(500);
} else {
const fileContent = JSON.stringify(versionedShareState);
const hash = createHash('md5');
hash.update(fileContent);
const digest = escapeUrl(hash.digest('base64'));
const filename = `${digest}.txt`;
storage.write(filename, fileContent);
res.send({ hash: digest });
}
} catch (ex) {
logger.error(ex);
res.sendStatus(500);
}
}
}

View File

@ -0,0 +1,36 @@
import { Request, Response } from 'express';
import { loadDefaultState } from '../load-state';
import latestSchema from '../schemas/share-latest';
import { storage } from '../storage';
import { FileNotFoundError } from '../storage/interface';
import { logger } from '../logger';
export function createSharedLinkHandler(
appBundleDirectory: string,
template: (state: string) => string
) {
return async (req: Request, res: Response) => {
try {
const content = await storage.read(`${req.params['hash']}.txt`);
const storedState = JSON.parse(content);
const migratedState = latestSchema.forward(storedState);
const defaultState = await loadDefaultState(appBundleDirectory);
const state = {
...defaultState,
...migratedState.state,
share: { link: req.params['hash'] }
};
res.send(template(JSON.stringify(state)));
} catch (ex) {
if (ex instanceof FileNotFoundError) {
res.sendStatus(404);
} else {
logger.error(ex);
res.sendStatus(500);
}
}
};
}

View File

@ -0,0 +1,64 @@
import express from 'express';
import fs from 'fs';
import { dirname, join } from 'path';
import { compileContractHandler } from './handlers/compile-contract';
import { compileExpressionHandler } from './handlers/compile-expression';
import { deployHandler } from './handlers/deploy';
import { dryRunHandler } from './handlers/dry-run';
import { evaluateValueHandler } from './handlers/evaluate-value';
import { runFunctionHandler } from './handlers/run-function';
import { shareHandler } from './handlers/share';
import { createSharedLinkHandler } from './handlers/shared-link';
import { loadDefaultState } from './load-state';
import { loggerMiddleware, errorLoggerMiddleware } from './logger';
var bodyParser = require('body-parser');
var escape = require('escape-html');
const app = express();
const port = 8080;
const appRootDirectory =
process.env['STATIC_ASSETS'] ||
dirname(require.resolve('../../client/package.json'));
const appBundleDirectory = join(appRootDirectory, 'build');
app.use(bodyParser.json());
app.use(loggerMiddleware);
const file = fs.readFileSync(join(appBundleDirectory, 'index.html'));
const template = (defaultState: string = '{}') => {
return file.toString().replace(
`<div id="root"></div>`,
// Injecting a script that contains a default state (Might want to refactor this if we do ssr)
// Adding an div containing the initial state this is vulnerable to xss
// To avoid vulnerability we escape it and then parse the content into a global variable
`
<input type="hidden" id="initialState" value="${escape(defaultState)}" />
<div id="root"></div>
<script>var defaultServerState = JSON.parse(document.getElementById("initialState").value); document.getElementById("initialState").remove()</script>`
);
};
app.use('^/$', async (_, res) =>
res.send(template(JSON.stringify(await loadDefaultState(appBundleDirectory))))
);
app.use(express.static(appBundleDirectory));
app.get(
`/p/:hash([0-9a-zA-Z\-\_]+)`,
createSharedLinkHandler(appBundleDirectory, template)
);
app.post('/api/compile-contract', compileContractHandler);
app.post('/api/compile-expression', compileExpressionHandler);
app.post('/api/dry-run', dryRunHandler);
app.post('/api/share', shareHandler);
app.post('/api/evaluate-value', evaluateValueHandler);
app.post('/api/run-function', runFunctionHandler);
app.post('/api/deploy', deployHandler);
app.use(errorLoggerMiddleware);
app.listen(port, () => {
console.log(`Listening on: ${port}`);
});

View File

@ -0,0 +1,212 @@
import fs from 'fs';
import path from 'path';
import tmp from 'tmp';
import { logger } from './logger';
const { spawn } = require('child_process');
const dataDir = process.env['DATA_DIR'] || path.join(__dirname, 'tmp');
const JOB_TIMEOUT = 10000;
export class CompilerError extends Error {
constructor(message: string) {
super(message);
}
}
export class LigoCompiler {
private ligoCmd = process.env['LIGO_CMD'] || [
'docker',
'run',
'-t',
'--rm',
'-v',
`${dataDir}:${dataDir}`,
'-w',
dataDir,
'ligolang/ligo:next'
];
private execPromise(cmd: string | string[], args: string[]): Promise<string> {
let command: string[] = [];
if (Array.isArray(cmd)) {
command = cmd;
} else {
command = cmd.split(' ');
}
let program = command[0];
const argument = [...command.slice(1), ...args];
return new Promise((resolve, reject) => {
try {
const result = spawn(program, argument, { shell: false, cwd: dataDir });
let finalResult = '';
let finalError = '';
result.stdout.on('data', (data: Buffer) => {
finalResult += data.toString();
});
result.stderr.on('data', (data: Buffer) => {
finalError += data.toString();
});
result.on('close', (code: any) => {
if (code === 0) {
resolve(finalResult);
} else {
reject(new CompilerError(finalError));
}
});
} catch (ex) {
logger.error(`Unexpected compiler error ${ex}`);
reject(ex);
}
setTimeout(() => {
reject(new Error(`command: ${cmd} Timed out after ${JOB_TIMEOUT} ms`));
}, JOB_TIMEOUT);
});
}
private createTemporaryFile(fileContent: string) {
return new Promise<{ name: string; remove: () => void }>(
(resolve, reject) => {
tmp.file(
{ dir: dataDir, postfix: '.ligo' },
(err, name, fd, remove) => {
if (err) {
reject(err);
return;
}
fs.write(fd, Buffer.from(fileContent), err => {
if (err) {
reject(err);
return;
}
resolve({
name,
remove: () => {
try {
remove();
} catch (ex) {
logger.error(`Unable to remove file ${name}`);
}
const ppFile = name.replace('.ligo', '.pp.ligo');
try {
if (fs.existsSync(ppFile)) {
fs.unlinkSync(ppFile);
}
} catch (ex) {
logger.error(`Unable to remove file ${ppFile}`);
}
}
});
});
}
);
}
);
}
async compileContract(
syntax: string,
code: string,
entrypoint: string,
format: string
) {
const { name, remove } = await this.createTemporaryFile(code);
try {
const result = await this.execPromise(this.ligoCmd, [
'compile-contract',
'--michelson-format',
format,
'-s',
syntax,
name,
entrypoint
]);
return result;
} finally {
remove();
}
}
async compileExpression(syntax: string, expression: string, format: string) {
const result = await this.execPromise(this.ligoCmd, [
'compile-expression',
'--michelson-format',
format,
syntax,
expression
]);
return result;
}
async dryRun(
syntax: string,
code: string,
entrypoint: string,
parameter: string,
storage: string
) {
const { name, remove } = await this.createTemporaryFile(code);
try {
const result = await this.execPromise(this.ligoCmd, [
'dry-run',
'-s',
syntax,
name,
entrypoint,
parameter,
storage
]);
return result;
} finally {
remove();
}
}
async evaluateValue(syntax: string, code: string, entrypoint: string) {
const { name, remove } = await this.createTemporaryFile(code);
try {
const result = await this.execPromise(this.ligoCmd, [
'evaluate-value',
'-s',
syntax,
name,
entrypoint
]);
return result;
} finally {
remove();
}
}
async runFunction(
syntax: string,
code: string,
entrypoint: string,
parameter: string
) {
const { name, remove } = await this.createTemporaryFile(code);
try {
const result = await this.execPromise(this.ligoCmd, [
'run-function',
'-s',
syntax,
name,
entrypoint,
parameter
]);
return result;
} finally {
remove();
}
}
}

View File

@ -0,0 +1,50 @@
import fs from 'fs';
import { join } from 'path';
function readFile(path: string): Promise<string> {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (error, content) => {
if (error) {
reject(error);
} else {
resolve(content);
}
});
});
}
export async function loadDefaultState(appBundleDirectory: string) {
const examples = await readFile(
join(appBundleDirectory, 'static', 'examples', 'list')
);
const examplesList = JSON.parse(examples);
const defaultState = {
compile: {},
dryRun: {},
deploy: {},
evaluateValue: {},
evaluateFunction: {},
editor: {},
examples: {
selected: null,
list: examplesList
}
};
if (examplesList[0]) {
const example = await readFile(
join(appBundleDirectory, 'static', 'examples', examplesList[0].id)
);
const defaultExample = JSON.parse(example);
defaultState.compile = defaultExample.compile;
defaultState.dryRun = defaultExample.dryRun;
defaultState.deploy = defaultExample.deploy;
defaultState.evaluateValue = defaultExample.evaluateValue;
defaultState.evaluateFunction = defaultExample.evaluateFunction;
defaultState.editor = defaultExample.editor;
defaultState.examples.selected = defaultExample;
}
return defaultState;
}

View File

@ -0,0 +1,24 @@
import { createLogger, format, transports } from 'winston';
const { combine, timestamp, simple } = format;
import expressWinston from 'express-winston';
interface Logger {
debug: (message: string) => void;
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
}
const config = {
format: combine(timestamp(), simple()),
transports: [new transports.Console()]
};
export const logger: Logger = createLogger(config);
export const loggerMiddleware = expressWinston.logger({
...config,
msg: 'HTTP {{req.method}} {{req.url}}',
requestWhitelist: [...expressWinston.requestWhitelist, 'body'],
responseWhitelist: [...expressWinston.responseWhitelist, 'body']
});
export const errorLoggerMiddleware = expressWinston.errorLogger(config);

View File

@ -0,0 +1,26 @@
import joi from '@hapi/joi';
export abstract class Migration {
protected abstract schema: joi.ObjectSchema;
protected abstract previous: Migration | null;
protected abstract migrate(data: any): any;
validate(data: any): joi.ValidationResult {
return this.schema.validate(data);
}
forward(data: any): any {
const { error, value } = this.validate(data);
if (error) {
if (this.previous) {
return this.migrate(this.previous.forward(data));
}
throw new Error(
`Unable to migrate ${data}. Reached the end of the migration chain.`
);
}
return value;
}
}

View File

@ -0,0 +1,3 @@
import { SchemaMigrationV1 } from './share-v1';
export default new SchemaMigrationV1();

View File

@ -0,0 +1,29 @@
import joi from '@hapi/joi';
import { Migration } from './migration';
export interface SchemaV0 {
code: string;
language: string;
entrypoint: string;
parameters: string;
storage: string;
}
export class SchemaMigrationV0 extends Migration {
protected readonly schema = joi.object({
code: joi.string().required(),
language: joi.string().required(),
entrypoint: joi.string().required(),
parameters: joi.any().required(),
storage: joi.any().required()
});
protected readonly previous: Migration | null = null;
protected migrate(_: any): any {
throw new Error(
'Called migrate() on the first migration. Cannot migrate v0 -> v0.'
);
}
}

View File

@ -0,0 +1,109 @@
import joi from '@hapi/joi';
import { Migration } from './migration';
import { SchemaMigrationV0, SchemaV0 } from './share-v0';
export type Version = 'v1';
export interface SchemaV1 {
version: Version;
state: {
editor: {
language: string;
code: string;
};
compile: {
entrypoint: string;
};
dryRun: {
entrypoint: string;
parameters: string;
storage: string;
};
deploy: {
entrypoint: string;
storage: string;
useTezBridge?: boolean;
};
evaluateValue: {
entrypoint: string;
};
evaluateFunction: {
entrypoint: string;
parameters: string;
};
};
}
export class SchemaMigrationV1 extends Migration {
readonly VERSION: Version = 'v1';
protected readonly schema = joi.object({
version: joi
.string()
.required()
.allow(this.VERSION),
state: joi.object({
editor: joi
.object({
language: joi.string().required(),
code: joi.string().required()
})
.required(),
compile: joi.object({
entrypoint: joi.string().allow('')
}),
dryRun: joi.object({
entrypoint: joi.string().allow(''),
parameters: joi.any().allow(''),
storage: joi.any().allow('')
}),
deploy: joi.object({
entrypoint: joi.string().allow(''),
storage: joi.any().allow(''),
useTezBridge: joi.boolean().optional()
}),
evaluateValue: joi.object({
entrypoint: joi.string().allow('')
}),
evaluateFunction: joi.object({
entrypoint: joi.string().allow(''),
parameters: joi.any().allow('')
})
})
});
protected readonly previous = new SchemaMigrationV0();
protected migrate(data: SchemaV0): SchemaV1 {
return {
version: this.VERSION,
state: {
editor: {
language: data.language,
code: data.code
},
compile: {
entrypoint: data.entrypoint
},
dryRun: {
entrypoint: data.entrypoint,
parameters: data.parameters,
storage: data.storage
},
deploy: {
entrypoint: data.entrypoint,
storage: data.storage,
useTezBridge: false
},
evaluateValue: {
entrypoint: data.entrypoint
},
evaluateFunction: {
entrypoint: data.entrypoint,
parameters: data.parameters
}
}
};
}
}

View File

@ -0,0 +1,13 @@
import fetch from 'node-fetch';
const URL = 'https://api.tez.ie/keys/babylonnet/';
const AUTHORIZATION_HEADER = 'Bearer ligo-ide';
export async function fetchRandomPrivateKey(): Promise<string> {
const response = await fetch(URL, {
method: 'POST',
headers: { 'Authorization': AUTHORIZATION_HEADER }
});
return response.text();
}

View File

@ -0,0 +1,23 @@
import fs from 'fs';
import { join } from 'path';
import { FileStorage, FileNotFoundError } from './interface';
export class DiskStorage implements FileStorage {
constructor(private readonly directory: string) {}
async write(filename: string, content: string) {
const path = join(this.directory, filename);
return fs.writeFileSync(path, content);
}
async read(filename: string): Promise<string> {
const path = join(this.directory, filename);
if (!fs.existsSync(path)) {
throw new FileNotFoundError(`File not found ${path}`);
}
const buf = fs.readFileSync(path);
return buf.toString();
}
}

View File

@ -0,0 +1,45 @@
import { FileStorage, FileNotFoundError } from './interface';
const { Storage } = require('@google-cloud/storage');
export class GoogleStorage implements FileStorage {
private storage = new Storage();
constructor(private readonly bucket: string) {}
async write(filename: string, content: string) {
const stream = this.storage
.bucket(this.bucket)
.file(filename)
.createWriteStream({});
stream.end(Buffer.from(content));
}
async read(filename: string): Promise<string> {
return new Promise((resolve, reject) => {
try {
const file = this.storage.bucket(this.bucket).file(filename);
file.exists(function(err: Error, exists: any) {
if (err) {
reject(err);
}
if (!exists) {
reject(new FileNotFoundError(`File not found ${filename}`));
} else {
const stream = file.createReadStream();
var buf = '';
stream
.on('data', function(d: string) {
buf += d;
})
.on('end', function() {
resolve(buf.toString());
});
}
});
} catch (ex) {
reject(ex);
}
});
}
}

Some files were not shown because too many files have changed in this diff Show More