Compare commits

...

19 Commits

Author SHA1 Message Date
muzam1l
25eec6980f Fix TrustSet transaction example. 2022-07-14 20:05:32 +05:30
muzamil
8e2f20c5ac Merge pull request #239 from XRPLF/feat/monaco-comp
Monaco component.
2022-07-14 13:51:24 +05:30
muzam1l
a3d094e873 Fix some more spelling errors. 2022-07-13 20:11:21 +05:30
muzam1l
ef70bfb13a Fix spelling error 2022-07-13 19:49:54 +05:30
muzam1l
c26c7c13d1 Improve compile error handling. 2022-07-13 16:50:47 +05:30
muzam1l
fc461ddd0d Content changed warning on deploy page. 2022-07-13 16:17:31 +05:30
muzam1l
c7001f6089 'Exit editor mode' button in wat editor. 2022-07-13 15:40:08 +05:30
muzam1l
243cbfec08 Upgrade hooks editor to comp. 2022-07-13 15:09:45 +05:30
muzam1l
1295e7fa41 Deploy editor to comp 2022-07-13 15:09:45 +05:30
muzam1l
793623d216 Migrate transaction json editor to comp. 2022-07-13 15:09:45 +05:30
muzam1l
0cde0eb240 Monaco component 2022-07-13 15:09:45 +05:30
muzamil
e2acb48e03 Merge pull request #236 from XRPLF/feat/jsdoc-to-ui
User declaration of input fields via JSDOC in script files.
2022-07-12 16:53:27 +05:30
muzamil
3fcbac5ed9 Merge pull request #231 from XRPLF/feat/create-account-name
Allow passing desired name while creating account.
2022-07-11 14:29:38 +05:30
Valtteri Karesto
3af2bad536 Make ping interval longer (#232)
* Make ping interval 45s

Co-authored-by: Vaclav Barta <vbarta@mangrove.cz>
2022-07-04 12:22:06 +02:00
muzam1l
4f1b877db0 Added optional tag to create account label. 2022-07-01 19:41:52 +05:30
muzam1l
53afb1d3d1 Fix html erros. 2022-07-01 17:08:34 +05:30
muzam1l
31ff7c0e28 Name field in import account dialog. 2022-07-01 16:43:15 +05:30
muzam1l
dfa35df465 reset input value on submit 2022-07-01 16:27:13 +05:30
muzam1l
f163b052e1 Allow passing desired name while creating account. 2022-07-01 14:33:28 +05:30
21 changed files with 350 additions and 200 deletions

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ yarn-error.log*
# vercel # vercel
.vercel .vercel
.vscode

View File

@@ -31,6 +31,7 @@ import transactionsData from "../content/transactions.json";
import { SetHookDialog } from "./SetHookDialog"; import { SetHookDialog } from "./SetHookDialog";
import { addFunds } from "../state/actions/addFaucetAccount"; import { addFunds } from "../state/actions/addFaucetAccount";
import { deleteHook } from "../state/actions/deployHook"; import { deleteHook } from "../state/actions/deployHook";
import { capitalize } from "../utils/helpers";
export const AccountDialog = ({ export const AccountDialog = ({
activeAccountAddress, activeAccountAddress,
@@ -42,12 +43,12 @@ export const AccountDialog = ({
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [showSecret, setShowSecret] = useState(false); const [showSecret, setShowSecret] = useState(false);
const activeAccount = snap.accounts.find( const activeAccount = snap.accounts.find(
(account) => account.address === activeAccountAddress account => account.address === activeAccountAddress
); );
return ( return (
<Dialog <Dialog
open={Boolean(activeAccountAddress)} open={Boolean(activeAccountAddress)}
onOpenChange={(open) => { onOpenChange={open => {
setShowSecret(false); setShowSecret(false);
!open && setActiveAccountAddress(null); !open && setActiveAccountAddress(null);
}} }}
@@ -99,7 +100,7 @@ export const AccountDialog = ({
tabIndex={-1} tabIndex={-1}
onClick={() => { onClick={() => {
const index = state.accounts.findIndex( const index = state.accounts.findIndex(
(acc) => acc.address === activeAccount?.address acc => acc.address === activeAccount?.address
); );
state.accounts.splice(index, 1); state.accounts.splice(index, 1);
}} }}
@@ -165,7 +166,7 @@ export const AccountDialog = ({
}} }}
ghost ghost
size="xs" size="xs"
onClick={() => setShowSecret((curr) => !curr)} onClick={() => setShowSecret(curr => !curr)}
> >
{showSecret ? "Hide" : "Show"} {showSecret ? "Hide" : "Show"}
</Button> </Button>
@@ -252,7 +253,7 @@ export const AccountDialog = ({
}} }}
> >
{activeAccount && activeAccount.hooks.length > 0 {activeAccount && activeAccount.hooks.length > 0
? activeAccount.hooks.map((i) => { ? activeAccount.hooks.map(i => {
return ( return (
<a <a
key={i} key={i}
@@ -301,7 +302,7 @@ interface AccountProps {
showHookStats?: boolean; showHookStats?: boolean;
} }
const Accounts: FC<AccountProps> = (props) => { const Accounts: FC<AccountProps> = props => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState< const [activeAccountAddress, setActiveAccountAddress] = useState<
string | null string | null
@@ -309,7 +310,7 @@ const Accounts: FC<AccountProps> = (props) => {
useEffect(() => { useEffect(() => {
const fetchAccInfo = async () => { const fetchAccInfo = async () => {
if (snap.clientStatus === "online") { if (snap.clientStatus === "online") {
const requests = snap.accounts.map((acc) => const requests = snap.accounts.map(acc =>
snap.client?.send({ snap.client?.send({
id: `hooks-builder-req-info-${acc.address}`, id: `hooks-builder-req-info-${acc.address}`,
command: "account_info", command: "account_info",
@@ -322,7 +323,7 @@ const Accounts: FC<AccountProps> = (props) => {
const balance = res?.account_data?.Balance as string; const balance = res?.account_data?.Balance as string;
const sequence = res?.account_data?.Sequence as number; const sequence = res?.account_data?.Sequence as number;
const accountToUpdate = state.accounts.find( const accountToUpdate = state.accounts.find(
(acc) => acc.address === address acc => acc.address === address
); );
if (accountToUpdate) { if (accountToUpdate) {
accountToUpdate.xrp = balance; accountToUpdate.xrp = balance;
@@ -330,7 +331,7 @@ const Accounts: FC<AccountProps> = (props) => {
accountToUpdate.error = null; accountToUpdate.error = null;
} else { } else {
const oldAccount = state.accounts.find( const oldAccount = state.accounts.find(
(acc) => acc.address === res?.account acc => acc.address === res?.account
); );
if (oldAccount) { if (oldAccount) {
oldAccount.xrp = "0"; oldAccount.xrp = "0";
@@ -341,7 +342,7 @@ const Accounts: FC<AccountProps> = (props) => {
} }
} }
}); });
const objectRequests = snap.accounts.map((acc) => { const objectRequests = snap.accounts.map(acc => {
return snap.client?.send({ return snap.client?.send({
id: `hooks-builder-req-objects-${acc.address}`, id: `hooks-builder-req-objects-${acc.address}`,
command: "account_objects", command: "account_objects",
@@ -352,7 +353,7 @@ const Accounts: FC<AccountProps> = (props) => {
objectResponses.forEach((res: any) => { objectResponses.forEach((res: any) => {
const address = res?.account as string; const address = res?.account as string;
const accountToUpdate = state.accounts.find( const accountToUpdate = state.accounts.find(
(acc) => acc.address === address acc => acc.address === address
); );
if (accountToUpdate) { if (accountToUpdate) {
accountToUpdate.hooks = accountToUpdate.hooks =
@@ -416,9 +417,7 @@ const Accounts: FC<AccountProps> = (props) => {
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text> <Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
</Heading> </Heading>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}> <Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
<Button ghost size="sm" onClick={() => addFaucetAccount(true)}> <ImportAccountDialog type="create" />
Create
</Button>
<ImportAccountDialog /> <ImportAccountDialog />
</Flex> </Flex>
</Flex> </Flex>
@@ -435,7 +434,7 @@ const Accounts: FC<AccountProps> = (props) => {
overflowY: "auto", overflowY: "auto",
}} }}
> >
{snap.accounts.map((account) => ( {snap.accounts.map(account => (
<Flex <Flex
column column
key={account.address + account.name} key={account.address + account.name}
@@ -488,7 +487,7 @@ const Accounts: FC<AccountProps> = (props) => {
{!props.hideDeployBtn && ( {!props.hideDeployBtn && (
<div <div
className="hook-deploy-button" className="hook-deploy-button"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
@@ -514,32 +513,71 @@ const Accounts: FC<AccountProps> = (props) => {
); );
}; };
export const transactionsOptions = transactionsData.map((tx) => ({ export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType, value: tx.TransactionType,
label: tx.TransactionType, label: tx.TransactionType,
})); }));
const ImportAccountDialog = () => { const ImportAccountDialog = ({
const [value, setValue] = useState(""); type = "import",
}: {
type?: "import" | "create";
}) => {
const [secret, setSecret] = useState("");
const [name, setName] = useState("");
const btnText = type === "import" ? "Import" : "Create";
const title = type === "import" ? "Import Account" : "Create Account";
const handleSubmit = async () => {
if (type === "create") {
const value = capitalize(name);
await addFaucetAccount(value, true);
setName("");
setSecret("");
return;
}
importAccount(secret, name);
setName("");
setSecret("");
};
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button ghost size="sm"> <Button ghost size="sm">
Import {btnText}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent aria-describedby={undefined}>
<DialogTitle>Import account</DialogTitle> <DialogTitle css={{ mb: "$4" }}>{title}</DialogTitle>
<DialogDescription> <Flex column>
<Label>Add account secret</Label> <Box css={{ mb: "$2" }}>
<Input <Label>
name="secret" Account name <Text muted>(optional)</Text>
type="password" </Label>
autoComplete="new-password" <Input
value={value} name="name"
onChange={(e) => setValue(e.target.value)} type="text"
/> autoComplete="off"
</DialogDescription> autoCapitalize="on"
value={name}
onChange={e => setName(e.target.value)}
/>
</Box>
{type === "import" && (
<Box>
<Label>Account secret</Label>
<Input
required
name="secret"
type="password"
autoComplete="new-password"
value={secret}
onChange={e => setSecret(e.target.value)}
/>
</Box>
)}
</Flex>
<Flex <Flex
css={{ css={{
@@ -552,14 +590,8 @@ const ImportAccountDialog = () => {
<Button outline>Cancel</Button> <Button outline>Cancel</Button>
</DialogClose> </DialogClose>
<DialogClose asChild> <DialogClose asChild>
<Button <Button type="submit" variant="primary" onClick={handleSubmit}>
variant="primary" {title}
onClick={() => {
importAccount(value);
setValue("");
}}
>
Import account
</Button> </Button>
</DialogClose> </DialogClose>
</Flex> </Flex>

View File

@@ -87,7 +87,7 @@ const addListeners = (account: ISelect | null) => {
if (streamState.socket) { if (streamState.socket) {
interval = setInterval(() => { interval = setInterval(() => {
streamState.socket?.send(""); streamState.socket?.send("");
}, 10000); }, 45000);
} }
streamState.socket.addEventListener("open", () => onOpen(account)); streamState.socket.addEventListener("open", () => onOpen(account));

View File

@@ -1,7 +1,6 @@
import React, { useRef, useState } from "react"; import React, { useState } from "react";
import { useSnapshot, ref } from "valtio"; import { useSnapshot } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import NextLink from "next/link"; import NextLink from "next/link";
@@ -10,24 +9,16 @@ import filesize from "filesize";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import state from "../state"; import state from "../state";
import wat from "../utils/wat-highlight"; import wat from "../utils/wat-highlight";
import EditorNavigation from "./EditorNavigation"; import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex } from "."; import { Button, Text, Link, Flex } from ".";
import Monaco from "./Monaco";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]; const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
const DeployEditor = () => { const DeployEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state); const snap = useSnapshot(state);
const router = useRouter(); const router = useRouter();
const { theme } = useTheme(); const { theme } = useTheme();
@@ -36,7 +27,7 @@ const DeployEditor = () => {
const activeFile = snap.files[snap.active]?.compiledContent const activeFile = snap.files[snap.active]?.compiledContent
? snap.files[snap.active] ? snap.files[snap.active]
: snap.files.filter((file) => file.compiledContent)[0]; : snap.files.filter(file => file.compiledContent)[0];
const compiledSize = activeFile?.compiledContent?.byteLength || 0; const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color = const color =
compiledSize > FILESIZE_BREAKPOINTS[1] compiledSize > FILESIZE_BREAKPOINTS[1]
@@ -45,6 +36,10 @@ const DeployEditor = () => {
? "$warning" ? "$warning"
: "$success"; : "$success";
const isContentChanged =
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
// const hasDeployErros = activeFile && activeFile.containsErrors;
const CompiledStatView = activeFile && ( const CompiledStatView = activeFile && (
<Flex <Flex
column column
@@ -80,6 +75,12 @@ const DeployEditor = () => {
<Button variant="link" onClick={() => setShowContent(true)}> <Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file View as WAT-file
</Button> </Button>
{isContentChanged && (
<Text warning>
File contents were changed after last compile, compile again to
incorporate your latest changes in the build.
</Text>
)}
</Flex> </Flex>
); );
const NoContentView = !snap.loading && router.isReady && ( const NoContentView = !snap.loading && router.isReady && (
@@ -99,7 +100,7 @@ const DeployEditor = () => {
</Text> </Text>
); );
const isContent = const isContent =
snap.files?.filter((file) => file.compiledWatContent).length > 0 && snap.files?.filter(file => file.compiledWatContent).length > 0 &&
router.isReady; router.isReady;
return ( return (
<Box <Box
@@ -126,32 +127,38 @@ const DeployEditor = () => {
) : !showContent ? ( ) : !showContent ? (
CompiledStatView CompiledStatView
) : ( ) : (
<Editor <Monaco
className="hooks-editor" className="hooks-editor"
defaultLanguage={"wat"} defaultLanguage={"wat"}
language={"wat"} language={"wat"}
path={`file://tmp/c/${activeFile?.name}.wat`} path={`file://tmp/c/${activeFile?.name}.wat`}
value={activeFile?.compiledWatContent || ""} value={activeFile?.compiledWatContent || ""}
beforeMount={(monaco) => { beforeMount={monaco => {
monaco.languages.register({ id: "wat" }); monaco.languages.register({ id: "wat" });
monaco.languages.setLanguageConfiguration("wat", wat.config); monaco.languages.setLanguageConfiguration("wat", wat.config);
monaco.languages.setMonarchTokensProvider("wat", wat.tokens); monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}} }}
onMount={(editor, monaco) => { onMount={editor => {
editorRef.current = editor;
editor.updateOptions({ editor.updateOptions({
glyphMargin: true, glyphMargin: true,
readOnly: true, readOnly: true,
}); });
}} }}
theme={theme === "dark" ? "dark" : "light"} theme={theme === "dark" ? "dark" : "light"}
overlay={
<Flex
css={{
m: "$1",
ml: "auto",
fontSize: "$sm",
color: "$textMuted",
}}
>
<Link onClick={() => setShowContent(false)}>
Exit editor mode
</Link>
</Flex>
}
/> />
)} )}
</Container> </Container>

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio"; import { useSnapshot, ref } from "valtio";
import Editor from "@monaco-editor/react";
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
import { ArrowBendLeftUp } from "phosphor-react"; import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
@@ -8,8 +7,6 @@ import { useRouter } from "next/router";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { saveFile } from "../state/actions"; import { saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants"; import { apiHeaderFiles } from "../state/constants";
import state from "../state"; import state from "../state";
@@ -22,10 +19,12 @@ import { listen } from "@codingame/monaco-jsonrpc";
import ReconnectingWebSocket from "reconnecting-websocket"; import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs"; import docs from "../xrpl-hooks-docs/docs";
import Monaco from "./Monaco";
import { saveAllFiles } from '../state/actions/saveFile';
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path; const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) { if (apiHeaderFiles.find(h => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true }); editor.updateOptions({ readOnly: true });
} else { } else {
editor.updateOptions({ readOnly: false }); editor.updateOptions({ readOnly: false });
@@ -42,7 +41,7 @@ const setMarkers = (monacoE: typeof monaco) => {
.getModelMarkers({}) .getModelMarkers({})
// Filter out the markers that are hooks specific // Filter out the markers that are hooks specific
.filter( .filter(
(marker) => marker =>
typeof marker?.code === "string" && typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-" // Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-") marker?.code?.includes("hooks-")
@@ -56,16 +55,16 @@ const setMarkers = (monacoE: typeof monaco) => {
// Add decoration (aka extra hoverMessages) to markers in the // Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are // exact same range (location) where the markers are
const models = monacoE.editor.getModels(); const models = monacoE.editor.getModels();
models.forEach((model) => { models.forEach(model => {
decorations[model.id] = model?.deltaDecorations( decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [], decorations?.[model.id] || [],
markers markers
.filter((marker) => .filter(marker =>
marker?.resource.path marker?.resource.path
.split("/") .split("/")
.includes(`${state.files?.[state.active]?.name}`) .includes(`${state.files?.[state.active]?.name}`)
) )
.map((marker) => ({ .map(marker => ({
range: new monacoE.Range( range: new monacoE.Range(
marker.startLineNumber, marker.startLineNumber,
marker.startColumn, marker.startColumn,
@@ -113,6 +112,13 @@ const HooksEditor = () => {
setMarkers(monacoRef.current); setMarkers(monacoRef.current);
} }
}, [snap.active]); }, [snap.active]);
useEffect(() => {
return () => {
saveAllFiles();
};
}, []);
const file = snap.files[snap.active];
return ( return (
<Box <Box
css={{ css={{
@@ -127,16 +133,16 @@ const HooksEditor = () => {
> >
<EditorNavigation /> <EditorNavigation />
{snap.files.length > 0 && router.isReady ? ( {snap.files.length > 0 && router.isReady ? (
<Editor <Monaco
className="hooks-editor"
keepCurrentModel keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language} defaultLanguage={file?.language}
language={snap.files?.[snap.active]?.language} language={file?.language}
path={`file:///work/c/${snap.files?.[snap.active]?.name}`} path={`file:///work/c/${file?.name}`}
defaultValue={snap.files?.[snap.active]?.content} defaultValue={file?.content}
beforeMount={(monaco) => { // onChange={val => (state.files[snap.active].content = val)} // Auto save?
beforeMount={monaco => {
if (!snap.editorCtx) { if (!snap.editorCtx) {
snap.files.forEach((file) => snap.files.forEach(file =>
monaco.editor.createModel( monaco.editor.createModel(
file.content, file.content,
file.language, file.language,
@@ -161,7 +167,7 @@ const HooksEditor = () => {
// listen when the web socket is opened // listen when the web socket is opened
listen({ listen({
webSocket: webSocket as WebSocket, webSocket: webSocket as WebSocket,
onConnection: (connection) => { onConnection: connection => {
// create and start the language client // create and start the language client
const languageClient = createLanguageClient(connection); const languageClient = createLanguageClient(connection);
const disposable = languageClient.start(); const disposable = languageClient.start();
@@ -177,7 +183,6 @@ const HooksEditor = () => {
}); });
} }
// // hook editor to global state
// editor.updateOptions({ // editor.updateOptions({
// minimap: { // minimap: {
// enabled: false, // enabled: false,
@@ -186,10 +191,6 @@ const HooksEditor = () => {
// }); // });
if (!state.editorCtx) { if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor); state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
} }
}} }}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
@@ -217,13 +218,13 @@ const HooksEditor = () => {
}); });
// Hacky way to hide Peek menu // Hacky way to hide Peek menu
editor.onContextMenu((e) => { editor.onContextMenu(e => {
const host = const host =
document.querySelector<HTMLElement>(".shadow-root-host"); document.querySelector<HTMLElement>(".shadow-root-host");
const contextMenuItems = const contextMenuItems =
host?.shadowRoot?.querySelectorAll("li.action-item"); host?.shadowRoot?.querySelectorAll("li.action-item");
contextMenuItems?.forEach((k) => { contextMenuItems?.forEach(k => {
// If menu item contains "Peek" lets hide it // If menu item contains "Peek" lets hide it
if (k.querySelector(".action-label")?.textContent === "Peek") { if (k.querySelector(".action-label")?.textContent === "Peek") {
// @ts-expect-error // @ts-expect-error

75
components/Monaco.tsx Normal file
View File

@@ -0,0 +1,75 @@
import Editor, { loader, EditorProps, Monaco } from "@monaco-editor/react";
import { CSS } from "@stitches/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes";
import { FC, MutableRefObject, ReactNode } from "react";
import { Flex } from ".";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
export type MonacoProps = EditorProps & {
id?: string;
rootProps?: { css: CSS } & Record<string, any>;
overlay?: ReactNode;
editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor>;
monacoRef?: MutableRefObject<typeof monaco>;
};
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const Monaco: FC<MonacoProps> = ({
id,
path = `file:///${id}`,
className = id,
language = "json",
overlay,
editorRef,
monacoRef,
beforeMount,
rootProps,
...rest
}) => {
const { theme } = useTheme();
const setTheme = (monaco: Monaco) => {
monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme("light", light as any);
};
return (
<Flex
fluid
column
{...rootProps}
css={{
position: "relative",
height: "100%",
...rootProps?.css,
}}
>
<Editor
className={className}
language={language}
path={path}
beforeMount={monaco => {
beforeMount?.(monaco);
setTheme(monaco);
}}
theme={theme === "dark" ? "dark" : "light"}
{...rest}
/>
{overlay && (
<Flex
css={{ position: "absolute", bottom: 0, right: 0, width: "100%" }}
>
{overlay}
</Flex>
)}
</Flex>
);
};
export default Monaco;

View File

@@ -20,6 +20,11 @@ const Text = styled("span", {
color: "$error", color: "$error",
}, },
}, },
warning: {
true: {
color: "$warning",
},
},
monospace: { monospace: {
true: { true: {
fontFamily: "$monospace", fontFamily: "$monospace",
@@ -28,8 +33,8 @@ const Text = styled("span", {
block: { block: {
true: { true: {
display: "block", display: "block",
} },
} },
}, },
}); });

View File

@@ -1,9 +1,4 @@
import Editor, { loader, useMonaco } from "@monaco-editor/react";
import { FC, useCallback, useEffect, useState } from "react"; import { FC, useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import dark from "../../theme/editor/amy.json";
import light from "../../theme/editor/xcode_default.json";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state, { import state, {
prepareState, prepareState,
@@ -11,18 +6,13 @@ import state, {
TransactionState, TransactionState,
} from "../../state"; } from "../../state";
import Text from "../Text"; import Text from "../Text";
import Flex from "../Flex"; import { Flex, Link } from "..";
import { Link } from "..";
import { showAlert } from "../../state/actions/showAlert"; import { showAlert } from "../../state/actions/showAlert";
import { parseJSON } from "../../utils/json"; import { parseJSON } from "../../utils/json";
import { extractSchemaProps } from "../../utils/schema"; import { extractSchemaProps } from "../../utils/schema";
import amountSchema from "../../content/amount-schema.json"; import amountSchema from "../../content/amount-schema.json";
import Monaco from "../Monaco";
loader.config({ import type monaco from "monaco-editor";
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
interface JsonProps { interface JsonProps {
value?: string; value?: string;
@@ -40,7 +30,6 @@ export const TxJson: FC<JsonProps> = ({
}) => { }) => {
const { editorSettings, accounts } = useSnapshot(state); const { editorSettings, accounts } = useSnapshot(state);
const { editorValue = value, estimatedFee } = txState; const { editorValue = value, estimatedFee } = txState;
const { theme } = useTheme();
const [hasUnsaved, setHasUnsaved] = useState(false); const [hasUnsaved, setHasUnsaved] = useState(false);
const [currTxType, setCurrTxType] = useState<string | undefined>( const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value txState.selectedTransaction?.value
@@ -95,9 +84,6 @@ export const TxJson: FC<JsonProps> = ({
}); });
}; };
const path = `file:///${header}`;
const monaco = useMonaco();
const getSchemas = useCallback(async (): Promise<any[]> => { const getSchemas = useCallback(async (): Promise<any[]> => {
const txObj = transactionsData.find( const txObj = transactionsData.find(
td => td.TransactionType === currTxType td => td.TransactionType === currTxType
@@ -177,55 +163,63 @@ export const TxJson: FC<JsonProps> = ({
]; ];
}, [accounts, currTxType, estimatedFee, header]); }, [accounts, currTxType, estimatedFee, header]);
const [monacoInst, setMonacoInst] = useState<typeof monaco>();
useEffect(() => { useEffect(() => {
if (!monaco) return; if (!monacoInst) return;
getSchemas().then(schemas => { getSchemas().then(schemas => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true, validate: true,
schemas, schemas,
}); });
}); });
}, [getSchemas, monaco]); }, [getSchemas, monacoInst]);
return ( return (
<Flex <Monaco
fluid rootProps={{
column css: { height: "calc(100% - 45px)" },
css={{ height: "calc(100% - 45px)", position: "relative" }} }}
> language={"json"}
<Editor id={header}
className="hooks-editor" height="100%"
language={"json"} value={editorValue}
path={path} onChange={val => setState({ editorValue: val })}
height="100%" onMount={(editor, monaco) => {
beforeMount={monaco => { editor.updateOptions({
monaco.editor.defineTheme("dark", dark as any); minimap: { enabled: false },
monaco.editor.defineTheme("light", light as any); glyphMargin: true,
}} tabSize: editorSettings.tabSize,
value={editorValue} dragAndDrop: true,
onChange={val => setState({ editorValue: val })} fontSize: 14,
onMount={(editor, monaco) => { });
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
});
// register onExit cb setMonacoInst(monaco);
const model = editor.getModel(); // register onExit cb
model?.onWillDispose(() => onExit(model.getValue())); const model = editor.getModel();
}} model?.onWillDispose(() => onExit(model.getValue()));
theme={theme === "dark" ? "dark" : "light"} }}
/> overlay={
{hasUnsaved && ( hasUnsaved ? (
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}> <Flex
This file has unsaved changes.{" "} row
<Link onClick={() => saveState(editorValue, currTxType)}>save</Link>{" "} align="center"
<Link onClick={discardChanges}>discard</Link> css={{ fontSize: "$xs", color: "$textMuted", ml: 'auto' }}
</Text> >
)} <Text muted small>
</Flex> This file has unsaved changes.
</Text>
<Link
css={{ ml: "$1" }}
onClick={() => saveState(editorValue, currTxType)}
>
save
</Link>
<Link css={{ ml: "$1" }} onClick={discardChanges}>
discard
</Link>
</Flex>
) : undefined
}
/>
); );
}; };

View File

@@ -38,22 +38,23 @@ export const TxUI: FC<UIProps> = ({
txFields, txFields,
} = txState; } = txState;
const transactionsOptions = transactionsData.map((tx) => ({
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType, value: tx.TransactionType,
label: tx.TransactionType, label: tx.TransactionType,
})); }));
const accountOptions: SelectOption[] = accounts.map((acc) => ({ const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
})); }));
const destAccountOptions: SelectOption[] = accounts const destAccountOptions: SelectOption[] = accounts
.map((acc) => ({ .map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
})) }))
.filter((acc) => acc.value !== selectedAccount?.value); .filter(acc => acc.value !== selectedAccount?.value);
const [feeLoading, setFeeLoading] = useState(false); const [feeLoading, setFeeLoading] = useState(false);
@@ -97,32 +98,37 @@ export const TxUI: FC<UIProps> = ({
[estimateFee, handleSetField] [estimateFee, handleSetField]
); );
const handleChangeTxType = (tt: SelectOption) => { const handleChangeTxType = useCallback(
setState({ selectedTransaction: tt }); (tt: SelectOption) => {
setState({ selectedTransaction: tt });
const newState = resetOptions(tt.value); const newState = resetOptions(tt.value);
handleEstimateFee(newState, true); handleEstimateFee(newState, true);
}; },
[handleEstimateFee, resetOptions, setState]
);
const specialFields = ["TransactionType", "Account", "Destination"]; const specialFields = ["TransactionType", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter( const otherFields = Object.keys(txFields).filter(
(k) => !specialFields.includes(k) k => !specialFields.includes(k)
) as [keyof TxFields]; ) as [keyof TxFields];
const switchToJson = () => const switchToJson = () =>
setState({ editorSavedValue: null, viewType: "json" }); setState({ editorSavedValue: null, viewType: "json" });
// default tx
useEffect(() => { useEffect(() => {
if (selectedTransaction?.value) return;
const defaultOption = transactionsOptions.find( const defaultOption = transactionsOptions.find(
(tt) => tt.value === "Payment" tt => tt.value === "Payment"
); );
if (defaultOption) { if (defaultOption) {
handleChangeTxType(defaultOption); handleChangeTxType(defaultOption);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [handleChangeTxType, selectedTransaction?.value, transactionsOptions]);
}, []);
return ( return (
<Container <Container
@@ -204,7 +210,7 @@ export const TxUI: FC<UIProps> = ({
/> />
</Flex> </Flex>
)} )}
{otherFields.map((field) => { {otherFields.map(field => {
let _value = txFields[field]; let _value = txFields[field];
let value: string | undefined; let value: string | undefined;
@@ -255,7 +261,7 @@ export const TxUI: FC<UIProps> = ({
<Input <Input
type={isFee ? "number" : "text"} type={isFee ? "number" : "text"}
value={value} value={value}
onChange={(e) => { onChange={e => {
if (isFee) { if (isFee) {
const val = e.target.value const val = e.target.value
.replaceAll(".", "") .replaceAll(".", "")
@@ -267,7 +273,7 @@ export const TxUI: FC<UIProps> = ({
}} }}
onKeyPress={ onKeyPress={
isFee isFee
? (e) => { ? e => {
if (e.key === "." || e.key === ",") { if (e.key === "." || e.key === ",") {
e.preventDefault(); e.preventDefault();
} }

View File

@@ -40,9 +40,9 @@
{ {
"label": "Token", "label": "Token",
"body": { "body": {
"currency": "${1:13.1}", "currency": "${1:USD}",
"value": "${2:FOO}", "value": "${2:100}",
"description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}" "issuer": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
} }
} }
] ]

View File

@@ -212,9 +212,13 @@
"Fee": "12", "Fee": "12",
"Flags": 262144, "Flags": 262144,
"LastLedgerSequence": 8007750, "LastLedgerSequence": 8007750,
"Amount": { "LimitAmount": {
"$value": "100", "$type": "json",
"$type": "xrp" "$value": {
"currency": "USD",
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
"value": "100"
}
}, },
"Sequence": 12 "Sequence": 12
} }

View File

@@ -27,7 +27,7 @@ export const names = [
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint * new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
* is protected with CORS so that's why we did our own endpoint * is protected with CORS so that's why we did our own endpoint
*/ */
export const addFaucetAccount = async (showToast: boolean = false) => { export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
// Lets limit the number of faucet accounts to 5 for now // Lets limit the number of faucet accounts to 5 for now
if (state.accounts.length > 5) { if (state.accounts.length > 5) {
return toast.error("You can only have maximum 6 accounts"); return toast.error("You can only have maximum 6 accounts");
@@ -52,7 +52,7 @@ export const addFaucetAccount = async (showToast: boolean = false) => {
} }
const currNames = state.accounts.map(acc => acc.name); const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({ state.accounts.push({
name: names.filter(name => !currNames.includes(name))[0], name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(), xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address, address: json.address,
secret: json.secret, secret: json.secret,

View File

@@ -14,19 +14,21 @@ import { ref } from "valtio";
*/ */
export const compileCode = async (activeId: number) => { export const compileCode = async (activeId: number) => {
// Save the file to global state // Save the file to global state
saveFile(false); saveFile(false, activeId);
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) { if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error("Missing env!"); throw Error("Missing env!");
} }
// Bail out if we're already compiling // Bail out if we're already compiling
if (state.compiling) { if (state.compiling) {
// if compiling is ongoing return // if compiling is ongoing return // TODO Inform user about it.
return; return;
} }
// Set loading state to true // Set loading state to true
state.compiling = true; state.compiling = true;
state.logs = [] state.logs = []
const file = state.files[activeId]
try { try {
file.containsErrors = false
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, { const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: {
@@ -40,8 +42,8 @@ export const compileCode = async (activeId: number) => {
{ {
type: "c", type: "c",
options: state.compileOptions.optimizationLevel || '-O2', options: state.compileOptions.optimizationLevel || '-O2',
name: state.files[activeId].name, name: file.name,
src: state.files[activeId].content, src: file.content,
}, },
], ],
}), }),
@@ -49,15 +51,15 @@ export const compileCode = async (activeId: number) => {
const json = await res.json(); const json = await res.json();
state.compiling = false; state.compiling = false;
if (!json.success) { if (!json.success) {
state.logs.push({ type: "error", message: json.message }); const errors = [json.message]
if (json.tasks && json.tasks.length > 0) { if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => { json.tasks.forEach((task: any) => {
if (!task.success) { if (!task.success) {
state.logs.push({ type: "error", message: task?.console }); errors.push(task?.console)
} }
}); });
} }
return toast.error(`Couldn't compile!`, { position: "bottom-center" }); throw errors
} }
state.logs.push({ state.logs.push({
type: "success", type: "success",
@@ -67,8 +69,9 @@ export const compileCode = async (activeId: number) => {
}); });
// Decode base64 encoded wasm that is coming back from the endpoint // Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output); const bufferData = await decodeBinary(json.output);
state.files[state.active].compiledContent = ref(bufferData); file.compiledContent = ref(bufferData);
state.files[state.active].lastCompiled = new Date(); file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content
// Import wabt from and create human readable version of wasm file and // Import wabt from and create human readable version of wasm file and
// put it into state // put it into state
import("wabt").then((wabt) => { import("wabt").then((wabt) => {
@@ -84,10 +87,23 @@ export const compileCode = async (activeId: number) => {
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
state.logs.push({
type: "error", if (err instanceof Array && typeof err[0] === 'string') {
message: "Error occured while compiling!", err.forEach(message => {
}); state.logs.push({
type: "error",
message,
});
})
}
else {
state.logs.push({
type: "error",
message: "Something went wrong, check your connection try again later!",
});
}
state.compiling = false; state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true
} }
}; };

View File

@@ -189,7 +189,7 @@ export const deployHook = async (
console.log(err); console.log(err);
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: "Error occured while deploying", message: "Error occurred while deploying",
}); });
} }
if (currentAccount) { if (currentAccount) {
@@ -272,10 +272,10 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
toast.error("Error occured while deleting hoook", { id: toastId }); toast.error("Error occurred while deleting hook", { id: toastId });
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: "Error occured while deleting hook", message: "Error occurred while deleting hook",
}); });
} }
if (currentAccount) { if (currentAccount) {

View File

@@ -13,7 +13,7 @@ export const downloadAsZip = async () => {
const zipFileName = guessZipFileName(files); const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName); zipped.saveFile(zipFileName);
} catch (error) { } catch (error) {
toast.error('Error occured while creating zip file, try again later') toast.error('Error occurred while creating zip file, try again later')
} finally { } finally {
state.zipLoading = false state.zipLoading = false
} }

View File

@@ -5,7 +5,7 @@ import state from '../index';
import { names } from './addFaucetAccount'; import { names } from './addFaucetAccount';
// Adds test account to global state with secret key // Adds test account to global state with secret key
export const importAccount = (secret: string) => { export const importAccount = (secret: string, name?: string) => {
if (!secret) { if (!secret) {
return toast.error("You need to add secret!"); return toast.error("You need to add secret!");
} }
@@ -19,7 +19,7 @@ export const importAccount = (secret: string) => {
if (err?.message) { if (err?.message) {
toast.error(err.message) toast.error(err.message)
} else { } else {
toast.error('Error occured while importing account') toast.error('Error occurred while importing account')
} }
return; return;
} }
@@ -27,7 +27,7 @@ export const importAccount = (secret: string) => {
return toast.error(`Couldn't create account!`); return toast.error(`Couldn't create account!`);
} }
state.accounts.push({ state.accounts.push({
name: names[state.accounts.length], name: name || names[state.accounts.length],
address: account.address || "", address: account.address || "",
secret: account.secret.familySeed || "", secret: account.secret.familySeed || "",
xrp: "0", xrp: "0",

View File

@@ -2,14 +2,15 @@ import toast from "react-hot-toast";
import state from '../index'; import state from '../index';
// Saves the current editor content to global state // Saves the current editor content to global state
export const saveFile = (showToast: boolean = true) => { export const saveFile = (showToast: boolean = true, activeId?: number) => {
const editorModels = state.editorCtx?.getModels(); const editorModels = state.editorCtx?.getModels();
const sought = '/' + state.files[state.active].name; const sought = '/' + state.files[state.active].name;
const currentModel = editorModels?.find((editorModel) => { const currentModel = editorModels?.find((editorModel) => {
return editorModel.uri.path.endsWith(sought); return editorModel.uri.path.endsWith(sought);
}); });
const file = state.files[activeId || state.active]
if (state.files.length > 0) { if (state.files.length > 0) {
state.files[state.active].content = currentModel?.getValue() || ""; file.content = currentModel?.getValue() || "";
} }
if (showToast) { if (showToast) {
toast.success("Saved successfully", { position: "bottom-center" }); toast.success("Saved successfully", { position: "bottom-center" });

View File

@@ -13,9 +13,11 @@ export interface IFile {
name: string; name: string;
language: string; language: string;
content: string; content: string;
compiledValueSnapshot?: string
compiledContent?: ArrayBuffer | null; compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null; compiledWatContent?: string | null;
lastCompiled?: Date lastCompiled?: Date
containsErrors?: boolean
} }
export interface FaucetAccountRes { export interface FaucetAccountRes {

View File

@@ -118,7 +118,7 @@ export const prepareTransaction = (data: any) => {
// handle type: `json` // handle type: `json`
if (_value && typeof _value === "object" && _value.$type === "json") { if (_value && typeof _value === "object" && _value.$type === "json") {
if (typeof _value.$value === "object") { if (typeof _value.$value === "object") {
options[field] = _value.$value as any; options[field] = { ..._value.$value } as any;
} else { } else {
try { try {
options[field] = JSON.parse(_value.$value); options[field] = JSON.parse(_value.$value);
@@ -131,7 +131,7 @@ export const prepareTransaction = (data: any) => {
} }
} }
// delete unneccesary fields // delete unnecessary fields
if (options[field] === undefined) { if (options[field] === undefined) {
delete options[field]; delete options[field];
} }

View File

@@ -19,6 +19,6 @@ export const getErrors = (source?: string): Error | undefined => {
); );
if (!probs.length) return undefined if (!probs.length) return undefined
const errors = probs.map(prob => `[${prob.code}] on line ${prob.line}: ${prob.message}`) const errors = probs.map(prob => `[${prob.code}] on line ${prob.line}: ${prob.message}`)
const error = new Error(`The following error(s) occured while parsing JSDOC: \n${errors.join('\n')}`) const error = new Error(`The following error(s) occurred while parsing JSDOC: \n${errors.join('\n')}`)
return error return error
} }

View File

@@ -6,4 +6,10 @@ export const guessZipFileName = (files: File[]) => {
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.') let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
parts = parts.length > 1 ? parts.slice(0, -1) : parts parts = parts.length > 1 ? parts.slice(0, -1) : parts
return parts.join('') return parts.join('')
}
export const capitalize = (value?: string) => {
if (!value) return '';
return value[0].toLocaleUpperCase() + value.slice(1);
} }