Compare commits

...

41 Commits

Author SHA1 Message Date
Vaclav Barta
81e2a3673d fix for issue #39 2021-12-20 13:02:42 +01:00
Valtteri Karesto
fdb1eb01a4 Merge pull request #34 from eqlabs/feat/zip
Implemented download as zip.
2021-12-15 15:02:23 +02:00
Valtteri Karesto
920d359966 Merge pull request #38 from eqlabs/feat/controlled-dialog
Filename Dialog fixes and improvements.
2021-12-15 13:16:49 +02:00
muzam
9e1dbc8765 minor fixes 2021-12-15 15:49:55 +05:30
muzam
10ea77fd8d Error and loading states in download as zip. 2021-12-15 15:38:19 +05:30
muzam
50de7ebf15 Merge branch 'feat/controlled-dialog' into feat/zip 2021-12-14 22:19:05 +05:30
muzam
7db07e3f92 Merge branch 'main' into feat/controlled-dialog 2021-12-14 21:29:31 +05:30
Valtteri Karesto
6ad7c67672 hotfix: fixes faucet url 2021-12-14 16:36:37 +02:00
Valtteri Karesto
10f279a6b4 Merge pull request #33 from eqlabs/feat/add-language-client
Add example of language server
2021-12-14 16:17:16 +02:00
muzam
792c093cfd Input filename validation and default extension. 2021-12-14 16:43:43 +05:30
muzam
a11a641608 New file dialog confirms on pressing Enter. 2021-12-14 15:33:39 +05:30
Valtteri Karesto
c3bf31d993 Fetch accounts only on client side 2021-12-13 23:06:13 +02:00
Valtteri Karesto
67d1b72331 fix eslintrc.json 2021-12-13 23:04:11 +02:00
Valtteri Karesto
35bc89cf99 Few cleanups to code 2021-12-13 23:02:03 +02:00
Valtteri Karesto
380e196db2 Added quick comments about code 2021-12-13 22:54:57 +02:00
Valtteri Karesto
d67613c0cf Add disabled state to button if no compiled code 2021-12-13 22:27:21 +02:00
Valtteri Karesto
4d4b96bede Extract actions to separate files 2021-12-13 22:23:37 +02:00
Valtteri Karesto
59637e32fe Add a lot of functionality to state 2021-12-13 17:26:37 +02:00
Valtteri Karesto
82d0c8c5ff Update navigation logic 2021-12-13 17:26:25 +02:00
Valtteri Karesto
b41ee2198b Separate editors for deploy and develop 2021-12-13 17:26:13 +02:00
Valtteri Karesto
09c5aff1da Add deploy footer 2021-12-13 17:25:58 +02:00
Valtteri Karesto
d806a46f13 Add account component 2021-12-13 17:25:49 +02:00
Valtteri Karesto
6bcbb5d6df Add patterns 2021-12-13 17:23:45 +02:00
Valtteri Karesto
0d7a4aae10 Remove footer 2021-12-13 17:23:36 +02:00
Valtteri Karesto
276dfff2ba Add link and log components 2021-12-13 17:23:09 +02:00
Valtteri Karesto
eddb870f85 Update deploy and develope pages 2021-12-13 17:22:51 +02:00
Valtteri Karesto
3707a215bb Add custom faucet api page to prevent cors problems 2021-12-13 17:22:36 +02:00
Valtteri Karesto
fad5e13430 Do not show modal on sign-in page 2021-12-13 17:22:18 +02:00
Valtteri Karesto
df47158f29 Some fixes to button 2021-12-13 17:21:59 +02:00
Valtteri Karesto
51e4fed345 Forgot to add yarn lock 2021-12-13 17:21:47 +02:00
Valtteri Karesto
e471e8d7ef Add colors to theme 2021-12-13 17:21:40 +02:00
Valtteri Karesto
adc268c3cd Added some env info to readme 2021-12-13 17:21:28 +02:00
Valtteri Karesto
69e08abbc9 Add postinstall patch script 2021-12-13 17:21:09 +02:00
Valtteri Karesto
b8596ec7ce Add some lint settings 2021-12-13 17:20:35 +02:00
Valtteri Karesto
4fc7098e78 Fix styles 2021-12-13 17:16:43 +02:00
Valtteri Karesto
69c7865491 Added some helper utils 2021-12-13 17:16:34 +02:00
muzam
8ac7e82221 Implemented download as zip 2021-12-13 16:21:28 +05:30
Valtteri Karesto
5eea51744e Monkeypatch ripple-binary-code to support SetHook 2021-12-07 15:31:10 +02:00
Valtteri Karesto
dcf0598852 Update file uris 2021-11-29 10:58:04 +02:00
Valtteri Karesto
a7d04a28e4 Add example of language server 2021-11-27 00:06:26 +02:00
Valtteri Karesto
a0303ecfa4 Merge pull request #32 from eqlabs/feat/fix-scrollbars
Fix scrollbars
2021-11-26 15:23:13 +02:00
45 changed files with 3458 additions and 634 deletions

View File

@@ -1,3 +1,5 @@
NEXTAUTH_URL=https://example.com
GITHUB_SECRET=""
GITHUB_ID=""
GITHUB_ID=""
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"

View File

@@ -8,7 +8,9 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Getting Started
First, run the development server:
First, copy the `.env.example` to `.env.local` file, someone from the team can provide you your enviroment variables.
Then, run the development server:
```bash
npm run dev

464
components/Accounts.tsx Normal file
View File

@@ -0,0 +1,464 @@
import toast from "react-hot-toast";
import { useSnapshot } from "valtio";
import { ArrowSquareOut, Copy, Wallet, X } from "phosphor-react";
import React, { useEffect, useState } from "react";
import Dinero from "dinero.js";
import Button from "./Button";
import { addFaucetAccount, deployHook, importAccount } from "../state/actions";
import state from "../state";
import Box from "./Box";
import Container from "./Container";
import Heading from "./Heading";
import Stack from "./Stack";
import Text from "./Text";
import Flex from "./Flex";
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
DialogTrigger,
} from "./Dialog";
import { css } from "../stitches.config";
import { Input } from "./Input";
const labelStyle = css({
color: "$mauve10",
textTransform: "uppercase",
fontSize: "10px",
mb: "$0.5",
});
const AccountDialog = ({
activeAccountAddress,
setActiveAccountAddress,
}: {
activeAccountAddress: string | null;
setActiveAccountAddress: React.Dispatch<React.SetStateAction<string | null>>;
}) => {
const snap = useSnapshot(state);
const [showSecret, setShowSecret] = useState(false);
const activeAccount = snap.accounts.find(
(account) => account.address === activeAccountAddress
);
return (
<Dialog
open={Boolean(activeAccountAddress)}
onOpenChange={(open) => {
setShowSecret(false);
!open && setActiveAccountAddress(null);
}}
>
<DialogContent
css={{
backgroundColor: "$mauve1 !important",
border: "1px solid $mauve2",
".dark &": {
// backgroundColor: "$black !important",
},
p: "$3",
"&:before": {
content: " ",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
opacity: 0.2,
".dark &": {
opacity: 1,
},
zIndex: 0,
pointerEvents: "none",
backgroundImage: `url('/pattern-dark.svg'), url('/pattern-dark-2.svg')`,
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left, top right",
},
}}
>
<DialogTitle
css={{
display: "flex",
width: "100%",
alignItems: "center",
borderBottom: "1px solid $mauve6",
pb: "$3",
gap: "$3",
fontSize: "$md",
}}
>
<Wallet size="15px" /> {activeAccount?.name}
</DialogTitle>
<DialogDescription as="div" css={{ fontFamily: "$monospace" }}>
<Stack css={{ display: "flex", flexDirection: "column", gap: "$3" }}>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Account Address</Text>
<Text
css={{
fontFamily: "$monospace",
}}
>
{activeAccount?.address}
</Text>
</Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
<Button
size="sm"
ghost
css={{ mt: "$3" }}
onClick={() => {
navigator.clipboard.writeText(activeAccount?.address || "");
toast.success("Copied address to clipboard");
}}
>
<Copy size="15px" />
</Button>
</Flex>
</Flex>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Secret</Text>
<Text
as="div"
css={{
fontFamily: "$monospace",
display: "flex",
alignItems: "center",
}}
>
{showSecret
? activeAccount?.secret
: "•".repeat(activeAccount?.secret.length || 16)}{" "}
<Button
css={{
fontFamily: "$monospace",
lineHeight: 2,
mt: "2px",
ml: "$3",
}}
ghost
size="xs"
onClick={() => setShowSecret((curr) => !curr)}
>
{showSecret ? "Hide" : "Show"}
</Button>
</Text>
</Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
<Button
size="sm"
ghost
onClick={() => {
navigator.clipboard.writeText(activeAccount?.secret || "");
toast.success("Copied secret to clipboard");
}}
css={{ mt: "$3" }}
>
<Copy size="15px" />
</Button>
</Flex>
</Flex>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Balances & Objects</Text>
<Text
css={{
fontFamily: "$monospace",
}}
>
{Dinero({
amount: Number(activeAccount?.xrp || "0"),
precision: 6,
})
.toUnit()
.toLocaleString(undefined, {
style: "currency",
currency: "XRP",
currencyDisplay: "name",
})}
</Text>
</Flex>
<Flex css={{ marginLeft: "auto" }}>
<a
href={`https://hooks-testnet-explorer.xrpl-labs.com/${activeAccount?.address}`}
target="_blank"
rel="noreferrer noopener"
>
<Button
size="sm"
ghost
css={{ color: "$green11 !important", mt: "$3" }}
>
<ArrowSquareOut size="15px" />
</Button>
</a>
</Flex>
</Flex>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Installed Hooks</Text>
<Text
css={{
fontFamily: "$monospace",
}}
>
{activeAccount && activeAccount?.hooks?.length > 0
? activeAccount?.hooks
.map((i) => {
return `${i?.substring(0, 6)}...${i?.substring(
i.length - 4
)}`;
})
.join(", ")
: ""}
</Text>
</Flex>
</Flex>
</Stack>
</DialogDescription>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
);
};
const Accounts = () => {
const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState<
string | null
>(null);
useEffect(() => {
const fetchAccInfo = async () => {
if (snap.clientStatus === "online") {
const requests = snap.accounts.map((acc) =>
snap.client?.send({
id: acc.address,
command: "account_info",
account: acc.address,
})
);
const responses = await Promise.all(requests);
responses.forEach((res: any) => {
const address = res?.account_data?.Account as string;
const balance = res?.account_data?.Balance as string;
const sequence = res?.account_data?.Sequence as number;
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.xrp = balance;
accountToUpdate.sequence = sequence;
}
});
const objectRequests = snap.accounts.map((acc) => {
return snap.client?.send({
id: `${acc.address}-hooks`,
command: "account_objects",
account: acc.address,
});
});
const objectResponses = await Promise.all(objectRequests);
objectResponses.forEach((res: any) => {
const address = res?.account as string;
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.hooks = res.account_objects
.filter((ac: any) => ac?.LedgerEntryType === "Hook")
.map((oo: any) => oo.HookHash);
}
});
}
};
let fetchAccountInfoInterval: NodeJS.Timer;
if (snap.clientStatus === "online") {
fetchAccInfo();
fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 2000);
}
return () => {
if (snap.accounts.length > 0) {
if (fetchAccountInfoInterval) {
clearInterval(fetchAccountInfoInterval);
}
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [snap.accounts, snap.clientStatus]);
return (
<Box
as="div"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
width: "50%",
flexShrink: 0,
borderRight: "1px solid $mauve6",
}}
>
<Container css={{ px: 0, flexShrink: 1 }}>
<Flex css={{ py: "$3" }}>
<Heading
as="h3"
css={{
fontWeight: 300,
m: 0,
fontSize: "11px",
color: "$mauve12",
px: "$3",
textTransform: "uppercase",
alignItems: "center",
display: "inline-flex",
gap: "$3",
}}
>
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
</Heading>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
<Button ghost size="xs" onClick={() => addFaucetAccount(true)}>
Create
</Button>
<ImportAccountDialog />
</Flex>
</Flex>
<Box
as="div"
css={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
overflowY: "auto",
wordWrap: "break-word",
}}
>
<Stack css={{ flexDirection: "column", gap: 0 }}>
{snap.accounts.map((account) => (
<Flex
key={account.address + account.name}
onClick={() => setActiveAccountAddress(account.address)}
css={{
gap: "$3",
p: "$2 $3",
justifyContent: "center",
cursor: "pointer",
"@hover": {
"&:hover": {
background: "$mauve3",
},
},
}}
>
<Text>{account.name} </Text>
<Text css={{ color: "$mauve9" }}>
{account.address} (
{Dinero({
amount: Number(account?.xrp || "0"),
precision: 6,
})
.toUnit()
.toLocaleString(undefined, {
style: "currency",
currency: "XRP",
currencyDisplay: "name",
})}
)
</Text>
<Button
css={{ ml: "auto" }}
size="xs"
uppercase
isLoading={account.isLoading}
disabled={
account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length
}
variant="secondary"
onClick={(e) => {
e.stopPropagation();
deployHook(account);
}}
>
Deploy
</Button>
</Flex>
))}
</Stack>
</Box>
</Container>
<AccountDialog
activeAccountAddress={activeAccountAddress}
setActiveAccountAddress={setActiveAccountAddress}
/>
</Box>
);
};
const ImportAccountDialog = () => {
const [value, setValue] = useState("");
return (
<Dialog>
<DialogTrigger asChild>
<Button ghost size="xs">
Import
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Import account</DialogTitle>
<DialogDescription>
<label>Add account secret</label>
<Input
name="secret"
type="password"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => {
importAccount(value);
setValue("");
}}
>
Import account
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
);
};
export default Accounts;

View File

@@ -6,6 +6,7 @@ import Spinner from "./Spinner";
export const StyledButton = styled("button", {
// Reset
all: "unset",
position: "relative",
appereance: "none",
fontFamily: "$body",
alignItems: "center",
@@ -112,7 +113,31 @@ export const StyledButton = styled("button", {
boxShadow: "inset 0 0 0 1px $colors$pink8",
},
},
secondary: {
backgroundColor: `$purple9`,
boxShadow: "inset 0 0 0 1px $colors$purple9",
color: "$white",
"@hover": {
"&:hover": {
backgroundColor: "$purple10",
boxShadow: "inset 0 0 0 1px $colors$purple11",
},
},
"&:active": {
backgroundColor: "$purple8",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
"&:focus": {
boxShadow: "inset 0 0 0 2px $colors$purple12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
backgroundColor: "$mauve4",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
},
},
outline: {
true: {
backgroundColor: "transparent",
@@ -183,6 +208,18 @@ export const StyledButton = styled("button", {
},
},
},
{
outline: true,
variant: "secondary",
css: {
background: "transparent",
color: "$mauve12",
"&:hover": {
color: "$mauve12",
background: "$mauve5",
},
},
},
],
defaultVariants: {
size: "md",

100
components/DeployEditor.tsx Normal file
View File

@@ -0,0 +1,100 @@
import React, { useRef } from "react";
import { useSnapshot, ref } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import NextLink from "next/link";
import Box from "./Box";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import state from "../state";
import EditorNavigation from "./EditorNavigation";
import Text from "./Text";
import Link from "./Link";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const DeployEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state);
const router = useRouter();
const { theme } = useTheme();
return (
<Box
css={{
flex: 1,
display: "flex",
position: "relative",
flexDirection: "column",
backgroundColor: "$mauve3",
width: "100%",
}}
>
<EditorNavigation showWat />
{snap.files?.filter((file) => file.compiledWatContent).length > 0 &&
router.isReady ? (
<Editor
className="hooks-editor"
// keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language}
language={snap.files?.[snap.active]?.language}
path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
value={snap.files?.[snap.active]?.compiledWatContent || ""}
beforeMount={(monaco) => {
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) => {
editorRef.current = editor;
editor.updateOptions({
glyphMargin: true,
readOnly: true,
});
}}
theme={theme === "dark" ? "dark" : "light"}
/>
) : (
<Container
css={{
display: "flex",
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
{!snap.loading && router.isReady && (
<Text
css={{
mt: "-60px",
fontSize: "14px",
fontFamily: "$monospace",
maxWidth: "300px",
textAlign: "center",
}}
>
{`You haven't compiled any files yet, compile files on `}
<NextLink shallow href={`/develop/${router.query.slug}`} passHref>
<Link as="a">develop view</Link>
</NextLink>
</Text>
)}
</Container>
)}
</Box>
);
};
export default DeployEditor;

View File

@@ -1,16 +1,25 @@
import React from "react";
import React, { useRef, useLayoutEffect } from "react";
import { useSnapshot } from "valtio";
import { Play, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import Container from "./Container";
import Box from "./Box";
import LogText from "./LogText";
import { compileCode, state } from "../state";
import { Play, Prohibit } from "phosphor-react";
import { compileCode } from "../state/actions";
import state from "../state";
import Button from "./Button";
import Heading from "./Heading";
const Footer = () => {
const snap = useSnapshot(state);
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
useLayoutEffect(() => {
stayScrolled();
}, [snap.logs, stayScrolled]);
return (
<Box
as="footer"
@@ -45,6 +54,7 @@ const Footer = () => {
</Button>
<Box
as="pre"
ref={logRef}
css={{
display: "flex",
flexDirection: "column",

View File

@@ -70,7 +70,7 @@ const StyledTitle = styled(DialogPrimitive.Title, {
});
const StyledDescription = styled(DialogPrimitive.Description, {
margin: "10px 0 20px",
margin: "10px 0 10px",
color: "$mauve11",
fontSize: 15,
lineHeight: 1.5,

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import {
Plus,
Share,
@@ -26,12 +26,8 @@ import NewWindow from "react-new-window";
import { signOut, useSession } from "next-auth/react";
import { useSnapshot } from "valtio";
import {
createNewFile,
state,
syncToGist,
updateEditorSettings,
} from "../state";
import { createNewFile, syncToGist, updateEditorSettings, downloadAsZip } from "../state/actions";
import state from "../state";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
@@ -46,6 +42,7 @@ import {
import Flex from "./Flex";
import Stack from "./Stack";
import Input from "./Input";
import Text from "./Text";
import toast from "react-hot-toast";
import {
AlertDialog,
@@ -55,11 +52,22 @@ import {
AlertDialogCancel,
AlertDialogAction,
} from "./AlertDialog";
import { styled } from "../stitches.config";
const EditorNavigation = () => {
const DEFAULT_EXTENSION = ".c";
const ErrorText = styled(Text, {
color: "$red9",
mt: "$1",
display: "block",
});
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
const snap = useSnapshot(state);
const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null);
const [filename, setFilename] = useState("");
const { data: session, status } = useSession();
const [popup, setPopUp] = useState(false);
@@ -69,6 +77,37 @@ const EditorNavigation = () => {
setPopUp(false);
}
}, [session, popup]);
// when filename changes, reset error
useEffect(() => {
setNewfileError(null);
}, [filename, setNewfileError]);
const validateFilename = useCallback(
(filename: string): { error: string | null } => {
if (snap.files.find(file => file.name === filename)) {
return { error: "Filename already exists." };
}
// More checks in future
return { error: null };
},
[snap.files]
);
const handleConfirm = useCallback(() => {
// add default extension in case omitted
let _filename = filename.includes(".") ? filename : filename + DEFAULT_EXTENSION;
const chk = validateFilename(_filename);
if (chk.error) {
setNewfileError(`Error: ${chk.error}`);
return;
}
setIsNewfileDialogOpen(false);
createNewFile(_filename);
setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const files = snap.files;
return (
<Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex
@@ -91,100 +130,107 @@ const EditorNavigation = () => {
marginBottom: "-1px",
}}
>
{snap.files &&
snap.files.length > 0 &&
snap.files?.map((file, index) => (
<Button
size="sm"
outline={snap.active !== index}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{file.name}
<Box
as="span"
{files &&
files.length > 0 &&
files.map((file, index) => {
if (!file.compiledContent && showWat) {
return null;
}
return (
<Button
size="sm"
outline={showWat ? snap.activeWat !== index : snap.active !== index}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
span: {
visibility: "visible",
},
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active =
index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
</Button>
))}
{file.name}
{showWat && ".wat"}
{!showWat && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active = index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
);
})}
{!showWat && (
<Dialog open={isNewfileDialogOpen} onOpenChange={setIsNewfileDialogOpen}>
<DialogTrigger asChild>
<Button ghost size="sm" css={{ alignItems: "center", px: "$2", mr: "$3" }}>
<Plus size="16px" /> {snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<label>Filename</label>
<Input
value={filename}
onChange={e => setFilename(e.target.value)}
onKeyPress={e => {
if (e.key === "Enter") {
handleConfirm();
}
}}
/>
<ErrorText>{newfileError}</ErrorText>
</DialogDescription>
<Dialog>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<label>Filename</label>
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
/>
</DialogDescription>
<Flex
css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button
variant="primary"
onClick={() => {
createNewFile(filename);
// reset
setFilename("");
}}
onClick={handleConfirm}
>
Create file
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
)}
</Stack>
</Container>
</Flex>
@@ -195,9 +241,7 @@ const EditorNavigation = () => {
zIndex: 1,
}}
>
<Container
css={{ width: "unset", display: "flex", alignItems: "center" }}
>
<Container css={{ width: "unset", display: "flex", alignItems: "center" }}>
{status === "authenticated" ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -230,15 +274,10 @@ const EditorNavigation = () => {
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem disabled onClick={() => signOut()}>
<User size="16px" /> {session?.user?.username} (
{session?.user.name})
<User size="16px" /> {session?.user?.username} ({session?.user.name})
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open(
`http://gist.github.com/${session?.user.username}`
)
}
onClick={() => window.open(`http://gist.github.com/${session?.user.username}`)}
>
<ArrowSquareOut size="16px" />
Go to your Gist
@@ -252,12 +291,7 @@ const EditorNavigation = () => {
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button
outline
size="sm"
css={{ mr: "$3" }}
onClick={() => setPopUp(true)}
>
<Button outline size="sm" css={{ mr: "$3" }} onClick={() => setPopUp(true)}>
<GithubLogo size="16px" /> Login
</Button>
)}
@@ -296,7 +330,7 @@ const EditorNavigation = () => {
},
}}
>
<Button outline size="sm" css={{ alignItems: "center" }}>
<Button isLoading={snap.zipLoading} onClick={downloadAsZip} outline size="sm" css={{ alignItems: "center" }}>
<DownloadSimple size="16px" />
</Button>
<Button
@@ -304,9 +338,7 @@ const EditorNavigation = () => {
size="sm"
css={{ alignItems: "center" }}
onClick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/develop/${snap.gistId}`
);
navigator.clipboard.writeText(`${window.location.origin}/develop/${snap.gistId}`);
toast.success("Copied share link to clipboard!");
}}
>
@@ -336,7 +368,7 @@ const EditorNavigation = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownMenuItem disabled={snap.zipLoading} onClick={downloadAsZip}>
<DownloadSimple size="16px" /> Download as ZIP
</DropdownMenuItem>
<DropdownMenuItem
@@ -351,9 +383,7 @@ const EditorNavigation = () => {
Copy share link to clipboard
</DropdownMenuItem>
<DropdownMenuItem
disabled={
session?.user.username !== snap.gistOwner || !snap.gistId
}
disabled={session?.user.username !== snap.gistOwner || !snap.gistId}
onClick={() => {
syncToGist(session);
}}
@@ -379,21 +409,15 @@ const EditorNavigation = () => {
</DropdownMenu>
</Stack>
{popup && !session ? (
<NewWindow center="parent" url="/sign-in" />
) : null}
{popup && !session ? <NewWindow center="parent" url="/sign-in" /> : null}
</Container>
</Flex>
<AlertDialog
open={createNewAlertOpen}
onOpenChange={(value) => setCreateNewAlertOpen(value)}
>
<AlertDialog open={createNewAlertOpen} onOpenChange={value => setCreateNewAlertOpen(value)}>
<AlertDialogContent>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action will create new <strong>public</strong> Github Gist from
your current saved files. You can delete gist anytime from your
GitHub Gists page.
This action will create new <strong>public</strong> Github Gist from your current saved
files. You can delete gist anytime from your GitHub Gists page.
</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
<AlertDialogCancel asChild>
@@ -427,8 +451,8 @@ const EditorNavigation = () => {
type="number"
min="1"
value={editorSettings.tabSize}
onChange={(e) =>
setEditorSettings((curr) => ({
onChange={e =>
setEditorSettings(curr => ({
...curr,
tabSize: Number(e.target.value),
}))
@@ -438,18 +462,12 @@ const EditorNavigation = () => {
<Flex css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}>
<DialogClose asChild>
<Button
outline
onClick={() => updateEditorSettings(editorSettings)}
>
<Button outline onClick={() => updateEditorSettings(editorSettings)}>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => updateEditorSettings(editorSettings)}
>
<Button variant="primary" onClick={() => updateEditorSettings(editorSettings)}>
Save changes
</Button>
</DialogClose>

View File

@@ -1,6 +1,6 @@
import React, { useRef } from "react";
import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio";
import Editor from "@monaco-editor/react";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes";
@@ -10,16 +10,33 @@ import Box from "./Box";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { saveFile, state } from "../state";
import { saveFile } from "../state/actions";
import state from "../state";
import EditorNavigation from "./EditorNavigation";
import Text from "./Text";
import { MonacoServices } from "@codingame/monaco-languageclient";
import { createLanguageClient, createWebSocket } from "../utils/languageClient";
import { listen } from "@codingame/monaco-jsonrpc";
import ReconnectingWebSocket from "reconnecting-websocket";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const HooksEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const subscriptionRef = useRef<ReconnectingWebSocket | null>(null);
const snap = useSnapshot(state);
const router = useRouter();
const { theme } = useTheme();
useEffect(() => {
return () => {
subscriptionRef?.current?.close();
};
}, []);
return (
<Box
css={{
@@ -35,12 +52,62 @@ const HooksEditor = () => {
<EditorNavigation />
{snap.files.length > 0 && router.isReady ? (
<Editor
className="hooks-editor"
keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language}
language={snap.files?.[snap.active]?.language}
path={snap.files?.[snap.active]?.name}
path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
defaultValue={snap.files?.[snap.active]?.content}
beforeMount={(monaco) => {
if (!snap.editorCtx) {
snap.files.forEach((file) =>
monaco.editor.createModel(
file.content,
file.language,
monaco.Uri.parse(`file:///work/c/${file.name}`)
)
);
}
// create the web socket
if (!subscriptionRef.current) {
monaco.languages.register({
id: "c",
extensions: [".c", ".h"],
aliases: ["C", "c", "H", "h"],
mimetypes: ["text/plain"],
});
MonacoServices.install(monaco);
const webSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ""
);
subscriptionRef.current = webSocket;
// listen when the web socket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: (connection) => {
// create and start the language client
const languageClient = createLanguageClient(connection);
const disposable = languageClient.start();
connection.onClose(() => {
try {
// disposable.stop();
disposable.dispose();
} catch (err) {
console.log("err", err);
}
});
},
});
}
// // hook editor to global state
// editor.updateOptions({
// minimap: {
// enabled: false,
// },
// ...snap.editorSettings,
// });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
@@ -51,17 +118,16 @@ const HooksEditor = () => {
}}
onMount={(editor, monaco) => {
editorRef.current = editor;
// hook editor to global state
editor.updateOptions({
minimap: {
enabled: false,
glyphMargin: true,
lightbulb: {
enabled: true,
},
...snap.editorSettings,
});
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
() => {
saveFile(editor.getValue());
saveFile();
}
);
}}

8
components/Link.tsx Normal file
View File

@@ -0,0 +1,8 @@
import { styled } from "../stitches.config";
const StyledLink = styled("a", {
color: "CurrentColor",
textDecoration: "underline",
});
export default StyledLink;

108
components/LogBox.tsx Normal file
View File

@@ -0,0 +1,108 @@
import React, { useRef, useLayoutEffect } from "react";
import { Notepad, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link";
import Container from "./Container";
import Box from "./Box";
import Flex from "./Flex";
import LogText from "./LogText";
import { ILog } from "../state";
import Text from "./Text";
import Button from "./Button";
import Heading from "./Heading";
import Link from "./Link";
interface ILogBox {
title: string;
clearLog?: () => void;
logs: ILog[];
}
const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children }) => {
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
useLayoutEffect(() => {
stayScrolled();
}, [stayScrolled, logs]);
return (
<Flex
as="div"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
flex: 1,
}}
>
<Container css={{ px: 0, flexShrink: 1 }}>
<Flex css={{ py: "$3" }}>
<Heading
as="h3"
css={{
fontWeight: 300,
m: 0,
fontSize: "11px",
color: "$mauve12",
px: "$3",
textTransform: "uppercase",
alignItems: "center",
display: "inline-flex",
gap: "$3",
}}
>
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
{clearLog && (
<Button ghost size="xs" onClick={clearLog}>
<Prohibit size="14px" />
</Button>
)}
</Flex>
</Flex>
<Box
as="pre"
ref={logRef}
css={{
margin: 0,
// display: "inline-block",
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
px: "$3",
pb: "$2",
whiteSpace: "normal",
overflowY: "auto",
}}
>
{logs?.map((log, index) => (
<Box as="span" key={log.type + index}>
{/* <LogText capitalize variant={log.type}>
{log.type}:{" "}
</LogText> */}
<LogText variant={log.type}>
{log.message}{" "}
{log.link && (
<NextLink href={log.link} shallow passHref>
<Link as="a">{log.linkText}</Link>
</NextLink>
)}
</LogText>
</Box>
))}
{children}
</Box>
</Container>
</Flex>
);
};
export default LogBox;

View File

@@ -15,6 +15,9 @@ const Text = styled("span", {
error: {
color: "$red11",
},
success: {
color: "$green11",
},
},
capitalize: {
true: {

View File

@@ -12,7 +12,7 @@ import Flex from "./Flex";
import Container from "./Container";
import Box from "./Box";
import ThemeChanger from "./ThemeChanger";
import { state } from "../state";
import state from "../state";
import Heading from "./Heading";
import Text from "./Text";
import Spinner from "./Spinner";
@@ -97,217 +97,223 @@ const Navigation = () => {
</>
)}
</Flex>
<ButtonGroup css={{ marginLeft: "auto" }}>
<Dialog
open={snap.mainModalOpen}
onOpenChange={(open) => (state.mainModalOpen = open)}
>
<DialogTrigger asChild>
<Button outline>
<FolderOpen size="15px" />
</Button>
</DialogTrigger>
<DialogContent
css={{
maxWidth: "100%",
width: "80vw",
height: "80%",
backgroundColor: "$mauve1 !important",
overflowY: "auto",
p: 0,
}}
{router.isReady && (
<ButtonGroup css={{ marginLeft: "auto" }}>
<Dialog
open={snap.mainModalOpen}
onOpenChange={(open) => (state.mainModalOpen = open)}
>
<Flex
<DialogTrigger asChild>
<Button outline>
<FolderOpen size="15px" />
</Button>
</DialogTrigger>
<DialogContent
css={{
flexDirection: "column",
flex: 1,
height: "auto",
"@md": {
flexDirection: "row",
height: "100%",
},
maxWidth: "100%",
width: "80vw",
height: "80%",
backgroundColor: "$mauve1 !important",
overflowY: "auto",
p: 0,
}}
>
<Flex
css={{
borderBottom: "1px solid $colors$mauve5",
width: "100%",
flexDirection: "column",
p: "$7",
height: "100%",
flex: 1,
height: "auto",
"@md": {
width: "30%",
borderBottom: "0px",
borderRight: "1px solid $colors$mauve5",
flexDirection: "row",
height: "100%",
},
}}
>
<DialogTitle
<Flex
css={{
textTransform: "uppercase",
display: "inline-flex",
alignItems: "center",
gap: "$3",
fontSize: "$xl",
borderBottom: "1px solid $colors$mauve5",
width: "100%",
flexDirection: "column",
p: "$7",
height: "100%",
"@md": {
width: "30%",
borderBottom: "0px",
borderRight: "1px solid $colors$mauve5",
},
}}
>
<Logo width="30px" height="30px" /> XRPL Hooks Editor
</DialogTitle>
<DialogDescription as="div">
<Text
<DialogTitle
css={{
textTransform: "uppercase",
display: "inline-flex",
color: "inherit",
my: "$5",
mb: "$7",
alignItems: "center",
gap: "$3",
fontSize: "$xl",
}}
>
Hooks add smart contract functionality to the XRP
Ledger.
</Text>
<Flex
css={{ flexDirection: "column", gap: "$2", mt: "$2" }}
>
<Logo width="30px" height="30px" /> XRPL Hooks Editor
</DialogTitle>
<DialogDescription as="div">
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
color: "inherit",
my: "$5",
mb: "$7",
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://github.com/XRPL-Labs/xrpld-hooks"
>
<ArrowUpRight size="15px" /> Developing Hooks
Hooks add smart contract functionality to the XRP
Ledger.
</Text>
<Flex
css={{ flexDirection: "column", gap: "$2", mt: "$2" }}
>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://github.com/XRPL-Labs/xrpld-hooks"
>
<ArrowUpRight size="15px" /> Developing Hooks
</Text>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://xrpl-hooks.readme.io/docs"
>
<ArrowUpRight size="15px" /> Hooks documentation
</Text>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://xrpl.org/docs.html"
>
<ArrowUpRight size="15px" /> XRPL documentation
</Text>
</Flex>
</DialogDescription>
</Flex>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://xrpl-hooks.readme.io/docs"
>
<ArrowUpRight size="15px" /> Hooks documentation
</Text>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://xrpl.org/docs.html"
>
<ArrowUpRight size="15px" /> XRPL documentation
</Text>
</Flex>
</DialogDescription>
</Flex>
<Flex
css={{
display: "grid",
gridTemplateColumns: "1fr",
gridTemplateRows: "max-content",
flex: 1,
p: "$7",
gap: "$3",
alignItems: "flex-start",
flexWrap: "wrap",
backgroundImage: `url('/pattern.svg'), url('/pattern-2.svg')`,
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left, top right",
"@md": {
gridTemplateColumns: "1fr 1fr 1fr",
<Flex
css={{
display: "grid",
gridTemplateColumns: "1fr",
gridTemplateRows: "max-content",
},
}}
>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
flex: 1,
p: "$7",
gap: "$3",
alignItems: "flex-start",
flexWrap: "wrap",
backgroundImage: `url('/pattern.svg'), url('/pattern-2.svg')`,
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left, top right",
"@md": {
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "max-content",
},
}}
>
<Heading>Starter</Heading>
<Text>Just an empty starter with essential imports</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Accept</Heading>
<Text>
This hook just accepts any transaction coming through it
</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Accept</Heading>
<Text>
This hook just accepts any transaction coming through it
</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Starter</Heading>
<Text>
Just an empty starter with essential imports
</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Accept</Heading>
<Text>
This hook just accepts any transaction coming through
it
</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Accept</Heading>
<Text>
This hook just accepts any transaction coming through
it
</Text>
</PanelBox>
</Flex>
</Flex>
</Flex>
<DialogClose asChild>
<Box
css={{
position: "absolute",
top: "$1",
right: "$1",
cursor: "pointer",
background: "$mauve1",
display: "flex",
borderRadius: "$full",
p: "$1",
}}
>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
<ThemeChanger />
</ButtonGroup>
<DialogClose asChild>
<Box
css={{
position: "absolute",
top: "$1",
right: "$1",
cursor: "pointer",
background: "$mauve1",
display: "flex",
borderRadius: "$full",
p: "$1",
}}
>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
<ThemeChanger />
</ButtonGroup>
)}
</Flex>
<Flex
css={{

View File

@@ -2,6 +2,15 @@
module.exports = {
reactStrictMode: true,
images: {
domains: ['avatars.githubusercontent.com'],
domains: ["avatars.githubusercontent.com"],
},
}
webpack(config, { isServer }) {
config.resolve.alias["vscode"] = require.resolve(
"@codingame/monaco-languageclient/lib/vscode-compatibility"
);
if (!isServer) {
config.resolve.fallback.fs = false;
}
return config;
},
};

View File

@@ -6,9 +6,12 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"postinstall": "patch-package"
},
"dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.3.1",
"@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7",
@@ -17,20 +20,39 @@
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@radix-ui/react-id": "^0.1.1",
"@stitches/react": "^1.2.6-0",
"monaco-editor": "^0.29.1",
"file-saver": "^2.0.5",
"jszip": "^3.7.1",
"base64-js": "^1.5.1",
"dinero.js": "^1.9.1",
"monaco-editor": "^0.30.1",
"next": "^12.0.4",
"next-auth": "^4.0.0-beta.5",
"next-themes": "^0.0.15",
"normalize-url": "^7.0.2",
"octokit": "^1.7.0",
"pako": "^2.0.4",
"patch-package": "^6.4.7",
"phosphor-react": "^1.3.1",
"postinstall-postinstall": "^2.1.0",
"re-resizable": "^6.9.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-hot-keys": "^2.7.1",
"react-hot-toast": "^2.1.1",
"react-new-window": "^0.2.1",
"valtio": "^1.2.5"
"react-stay-scrolled": "^7.4.0",
"reconnecting-websocket": "^4.4.0",
"valtio": "^1.2.5",
"vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2",
"wabt": "1.0.16",
"xrpl-accountlib": "^1.2.3",
"xrpl-client": "^1.9.3"
},
"devDependencies": {
"@types/file-saver": "^2.0.4",
"@types/dinero.js": "^1.9.0",
"@types/pako": "^1.0.2",
"@types/react": "17.0.31",
"eslint": "7.32.0",
"eslint-config-next": "11.1.2",

View File

@@ -10,20 +10,19 @@ import { IdProvider } from "@radix-ui/react-id";
import { darkTheme, css } from "../stitches.config";
import Navigation from "../components/Navigation";
import { fetchFiles, state } from "../state";
import { fetchFiles } from "../state/actions";
import state from "../state";
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
const router = useRouter();
const slug = router.query?.slug;
const gistId = (Array.isArray(slug) && slug[0]) ?? null;
useEffect(() => {
if (router.pathname.includes("/develop")) {
if (gistId && router.isReady) {
fetchFiles(gistId);
} else {
if (!gistId && router.isReady) {
state.mainModalOpen = true;
}
if (gistId && router.isReady) {
fetchFiles(gistId);
} else {
if (!gistId && router.isReady && !router.pathname.includes("/sign-in")) {
state.mainModalOpen = true;
}
}
}, [gistId, router.isReady, router.pathname]);
@@ -52,6 +51,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
backgroundColor: "$mauve1",
color: "$mauve10",
fontSize: "$sm",
zIndex: 9999,
".dark &": {
backgroundColor: "$mauve4",
color: "$mauve12",

36
pages/api/faucet.ts Normal file
View File

@@ -0,0 +1,36 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
interface ErrorResponse {
error: string
}
export interface Faucet {
address: string;
secret: string;
xrp: number;
hash: string;
code: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Faucet | ErrorResponse>
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed!' })
}
try {
const response = await fetch('https://hooks-testnet.xrpl-labs.com/newcreds', { method: 'POST' });
const json: Faucet | ErrorResponse = await response.json();
if ("error" in json) {
return res.status(429).json(json)
}
return res.status(200).json(json);
} catch (err) {
console.log(err)
return res.status(500).json({ error: 'Server error' })
}
return res.status(500).json({ error: 'Not able to create faucet, try again' })
}

View File

@@ -1,8 +1,37 @@
import Container from "../../components/Container";
import React from "react";
import dynamic from "next/dynamic";
import Flex from "../../components/Flex";
import { useSnapshot } from "valtio";
import state from "../../state";
const DeployEditor = dynamic(() => import("../../components/DeployEditor"), {
ssr: false,
});
const Accounts = dynamic(() => import("../../components/Accounts"), {
ssr: false,
});
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Deploy = () => {
const snap = useSnapshot(state);
return (
<Container css={{ py: "$10" }}>This will be the deploy page</Container>
<>
<main style={{ display: "flex", flex: 1 }}>
<DeployEditor />
</main>
<Flex css={{ flexDirection: "row", width: "100%" }}>
<Accounts />
<LogBox
title="Deploy Log"
logs={snap.deployLogs}
clearLog={() => (state.deployLogs = [])}
/>
</Flex>
</>
);
};

View File

@@ -1,22 +1,70 @@
import dynamic from "next/dynamic";
import { useSnapshot } from "valtio";
import Hotkeys from "react-hot-keys";
import { Play } from "phosphor-react";
import type { NextPage } from "next";
import { compileCode } from "../../state/actions";
import state from "../../state";
import Button from "../../components/Button";
import Box from "../../components/Box";
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
ssr: false,
});
const Footer = dynamic(() => import("../../components/Footer"), {
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Home: NextPage = () => {
const snap = useSnapshot(state);
return (
<>
<main style={{ display: "flex", flex: 1 }}>
<main style={{ display: "flex", flex: 1, position: "relative" }}>
<HooksEditor />
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
"c" && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() =>
!snap.compiling && snap.files.length && compileCode(snap.active)
}
>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Hotkeys>
)}
</main>
<Footer />
<Box
css={{
display: "flex",
background: "$mauve1",
position: "relative",
}}
>
<LogBox
title="Development Log"
clearLog={() => (state.logs = [])}
logs={snap.logs}
/>
</Box>
</>
);
};

View File

@@ -0,0 +1,377 @@
diff --git a/node_modules/ripple-binary-codec/dist/enums/definitions.json b/node_modules/ripple-binary-codec/dist/enums/definitions.json
index 2333c42..99557cb 100644
--- a/node_modules/ripple-binary-codec/dist/enums/definitions.json
+++ b/node_modules/ripple-binary-codec/dist/enums/definitions.json
@@ -1,3 +1,4 @@
+
{
"TYPES": {
"Validation": 10003,
@@ -40,8 +41,7 @@
"Check": 67,
"Nickname": 110,
"Contract": 99,
- "NFTokenPage": 80,
- "NFTokenOffer": 55,
+ "GeneratorMap": 103,
"NegativeUNL": 78
},
"FIELDS": [
@@ -95,16 +95,6 @@
"type": "UInt16"
}
],
- [
- "TransferFee",
- {
- "nth": 4,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "UInt16"
- }
- ],
[
"Flags",
{
@@ -455,6 +445,16 @@
"type": "UInt32"
}
],
+ [
+ "EmitGeneration",
+ {
+ "nth": 43,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "UInt32"
+ }
+ ],
[
"IndexNext",
{
@@ -635,16 +635,6 @@
"type": "Hash256"
}
],
- [
- "TokenID",
- {
- "nth": 10,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Hash256"
- }
- ],
[
"BookDirectory",
{
@@ -916,7 +906,7 @@
}
],
[
- "URI",
+ "Generator",
{
"nth": 5,
"isVLEncoded": true,
@@ -1156,7 +1146,7 @@
}
],
[
- "Minter",
+ "EmitCallback",
{
"nth": 9,
"isVLEncoded": true,
@@ -1276,9 +1266,9 @@
}
],
[
- "NonFungibleToken",
+ "Signer",
{
- "nth": 12,
+ "nth": 16,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1286,9 +1276,9 @@
}
],
[
- "Signer",
+ "Majority",
{
- "nth": 16,
+ "nth": 18,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1296,9 +1286,9 @@
}
],
[
- "Majority",
+ "DisabledValidator",
{
- "nth": 18,
+ "nth": 19,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1306,9 +1296,9 @@
}
],
[
- "DisabledValidator",
+ "EmitDetails",
{
- "nth": 19,
+ "nth": 12,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1395,16 +1385,6 @@
"type": "STArray"
}
],
- [
- "NonFungibleTokens",
- {
- "nth": 10,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "STArray"
- }
- ],
[
"Majorities",
{
@@ -1535,16 +1515,6 @@
"type": "Vector256"
}
],
- [
- "TokenIDs",
- {
- "nth": 4,
- "isVLEncoded": true,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Vector256"
- }
- ],
[
"Transaction",
{
@@ -1616,9 +1586,9 @@
}
],
[
- "TokenTaxon",
+ "HookStateCount",
{
- "nth": 42,
+ "nth": 40,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1626,9 +1596,9 @@
}
],
[
- "MintedTokens",
+ "HookReserveCount",
{
- "nth": 43,
+ "nth": 41,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1636,9 +1606,9 @@
}
],
[
- "BurnedTokens",
+ "HookDataMaxSize",
{
- "nth": 44,
+ "nth": 42,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1646,29 +1616,29 @@
}
],
[
- "Channel",
+ "HookOn",
{
- "nth": 22,
+ "nth": 16,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
- "type": "Hash256"
+ "type": "UInt64"
}
],
[
- "ConsensusHash",
+ "EmitBurden",
{
- "nth": 23,
+ "nth": 12,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
- "type": "Hash256"
+ "type": "UInt64"
}
],
[
- "CheckID",
+ "Channel",
{
- "nth": 24,
+ "nth": 22,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1676,9 +1646,9 @@
}
],
[
- "ValidatedHash",
+ "ConsensusHash",
{
- "nth": 25,
+ "nth": 23,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1686,9 +1656,9 @@
}
],
[
- "PreviousPageMin",
+ "CheckID",
{
- "nth": 26,
+ "nth": 24,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1696,9 +1666,9 @@
}
],
[
- "NextPageMin",
+ "ValidatedHash",
{
- "nth": 27,
+ "nth": 25,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1706,9 +1676,9 @@
}
],
[
- "BuyOffer",
+ "EmitParentTxnID",
{
- "nth": 28,
+ "nth": 10,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1716,9 +1686,9 @@
}
],
[
- "SellOffer",
+ "EmitNonce",
{
- "nth": 29,
+ "nth": 11,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1754,36 +1724,6 @@
"isSigningField": true,
"type": "UInt64"
}
- ],
- [
- "Cookie",
- {
- "nth": 10,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "UInt64"
- }
- ],
- [
- "ServerVersion",
- {
- "nth": 11,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "UInt64"
- }
- ],
- [
- "OfferNode",
- {
- "nth": 12,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "UInt64"
- }
]
],
"TRANSACTION_RESULTS": {
@@ -1908,18 +1848,7 @@
"tecDUPLICATE": 149,
"tecKILLED": 150,
"tecHAS_OBLIGATIONS": 151,
- "tecTOO_SOON": 152,
-
- "tecMAX_SEQUENCE_REACHED": 154,
- "tecNO_SUITABLE_PAGE": 155,
- "tecBUY_SELL_MISMATCH": 156,
- "tecOFFER_TYPE_MISMATCH": 157,
- "tecCANT_ACCEPT_OWN_OFFER": 158,
- "tecINSUFFICIENT_FUNDS": 159,
- "tecOBJECT_NOT_FOUND": 160,
- "tecINSUFFICIENT_PAYMENT": 161,
- "tecINCORRECT_ASSET": 162,
- "tecTOO_MANY": 163
+ "tecTOO_SOON": 152
},
"TRANSACTION_TYPES": {
"Invalid": -1,
@@ -1946,11 +1875,10 @@
"DepositPreauth": 19,
"TrustSet": 20,
"AccountDelete": 21,
- "NFTokenMint": 25,
- "NFTokenBurn": 26,
- "NFTokenCreateOffer": 27,
- "NFTokenCancelOffer": 28,
- "NFTokenAcceptOffer": 29,
+ "SetHook": 22,
+ "Invoke": 23,
+ "Batch": 24,
+
"EnableAmendment": 100,
"SetFee": 101,
"UNLModify": 102

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 152 KiB

9
public/pattern-dark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 153 KiB

254
state.ts
View File

@@ -1,254 +0,0 @@
import { proxy } from 'valtio';
import { devtools } from 'valtio/utils';
import { Octokit } from '@octokit/core';
import type monaco from 'monaco-editor';
import toast from 'react-hot-toast';
import Router from 'next/router';
import type { Session } from 'next-auth';
const octokit = new Octokit();
interface File {
name: string;
language: string;
content: string;
}
interface IState {
files: File[],
gistId?: string | null,
gistOwner?: string | null,
gistName?: string | null,
active: number;
loading: boolean;
gistLoading: boolean;
compiling: boolean;
logs: {
type: 'error' | 'warning' | 'log',
message: string;
}[];
editorCtx?: typeof monaco.editor;
editorSettings: {
tabSize: number;
},
mainModalOpen: boolean;
}
// let localStorageState: null | string = null;
let initialState = {
files: [],
active: 0,
loading: false,
compiling: false,
logs: [],
editorCtx: undefined,
gistId: undefined,
gistOwner: undefined,
gistName: undefined,
gistLoading: false,
editorSettings: {
tabSize: 2
},
mainModalOpen: false
}
// Check if there's a persited state in localStorage
// if (typeof window !== 'undefined') {
// try {
// localStorageState = localStorage.getItem('hooksIdeState');
// } catch (err) {
// console.log(`localStorage state broken`);
// localStorage.removeItem('hooksIdeState');
// }
// }
// if (localStorageState) {
// initialState = JSON.parse(localStorageState);
// }
// Initialize state
export const state = proxy<IState>({ ...initialState, logs: [] });
// Fetch content from Githug Gists
export const fetchFiles = (gistId: string) => {
state.loading = true;
if (gistId) {
state.logs.push({ type: 'log', message: `Fetching Gist with id: ${gistId}` });
octokit.request("GET /gists/{gist_id}", { gist_id: gistId }).then(res => {
if (res.data.files && Object.keys(res.data.files).length > 0) {
const files = Object.keys(res.data.files).map(filename => ({
name: res.data.files?.[filename]?.filename || 'noname.c',
language: res.data.files?.[filename]?.language?.toLowerCase() || '',
content: res.data.files?.[filename]?.content || ''
}))
state.loading = false;
if (files.length > 0) {
state.logs.push({ type: 'log', message: 'Fetched successfully ✅' })
state.files = files;
state.gistId = gistId;
state.gistName = Object.keys(res.data.files)?.[0] || 'untitled';
state.gistOwner = res.data.owner?.login;
return
} else {
// Open main modal if now files
state.mainModalOpen = true;
}
return Router.push({ pathname: '/develop' })
}
state.loading = false;
}).catch(err => {
state.loading = false;
state.logs.push({ type: 'error', message: `Couldn't find Gist with id: ${gistId}` })
return
})
return
}
state.loading = false;
// return state.files = initFiles
}
export const syncToGist = async (session?: Session | null, createNewGist?: boolean) => {
let files: Record<string, { filename: string, content: string }> = {};
state.gistLoading = true;
if (!session || !session.user) {
state.gistLoading = false;
return toast.error('You need to be logged in!')
}
const toastId = toast.loading('Pushing to Gist');
if (!state.files || !state.files.length) {
state.gistLoading = false;
return toast.error(`You need to create some files we can push to gist`, { id: toastId })
}
if (state.gistId && session?.user.username === state.gistOwner && !createNewGist) {
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", { gist_id: state.gistId });
if (currentFilesRes.data.files) {
Object.keys(currentFilesRes?.data?.files).forEach(filename => {
files[`${filename}`] = { filename, content: "" }
})
}
state.files.forEach(file => {
files[`${file.name}`] = { filename: file.name, content: file.content }
})
// Update existing Gist
octokit.request("PATCH /gists/{gist_id}", {
gist_id: state.gistId, files, headers: {
authorization: `token ${session?.accessToken || ''}`
}
}).then(res => {
state.gistLoading = false;
return toast.success('Updated to gist successfully!', { id: toastId })
}).catch(err => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not update Gist, try again later!`, { id: toastId })
})
} else {
// Not Gist of the current user or it isn't Gist yet
state.files.forEach(file => {
files[`${file.name}`] = { filename: file.name, content: file.content }
})
octokit.request("POST /gists", {
files,
public: true,
headers: {
authorization: `token ${session?.accessToken || ''}`
}
}).then(res => {
state.gistLoading = false;
state.gistOwner = res.data.owner?.login;
state.gistId = res.data.id;
state.gistName = Array.isArray(res.data.files) ? Object.keys(res.data?.files)?.[0] : 'Untitled';
Router.push({ pathname: `/develop/${res.data.id}` })
return toast.success('Created new gist successfully!', { id: toastId })
}).catch(err => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not create Gist, try again later!`, { id: toastId })
})
}
}
export const updateEditorSettings = (editorSettings: IState['editorSettings']) => {
state.editorCtx?.getModels().forEach(model => {
model.updateOptions({
...editorSettings
})
});
return state.editorSettings = editorSettings;
}
export const saveFile = (value: string) => {
const editorModels = state.editorCtx?.getModels();
const currentModel = editorModels?.find(editorModel => editorModel.uri.path === `/${state.files[state.active].name}`);
if (state.files.length > 0) {
state.files[state.active].content = currentModel?.getValue() || '';
}
toast.success('Saved successfully', { position: 'bottom-center' })
}
export const createNewFile = (name: string) => {
const emptyFile: File = { name, language: 'c', content: "" };
state.files.push(emptyFile)
state.active = state.files.length - 1;
}
export const compileCode = async (activeId: number) => {
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error('Missing env!')
};
if (state.compiling) {
return;
}
state.compiling = true;
try {
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"output": "wasm",
"compress": true,
"files": [
{
"type": "c",
"name": state.files[activeId].name,
"options": "-g -O3",
"src": state.files[activeId].content
}
]
})
});
const json = await res.json();
state.compiling = false;
if (!json.success) {
state.logs.push({ type: 'error', message: json.message })
if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => {
if (!task.success) {
state.logs.push({ type: 'error', message: task?.console })
}
})
}
return toast.error(`Couldn't compile!`, { position: 'bottom-center' });
}
state.logs.push({ type: 'log', message: 'Compiled successfully ✅' })
toast.success('Compiled successfully!', { position: 'bottom-center' });
} catch (err) {
console.log(err)
state.logs.push({ type: 'error', message: 'Error occured while compiling!' })
state.compiling = false;
}
}
if (process.env.NODE_ENV !== 'production') {
devtools(state, 'Files State');
}
// subscribe(state, () => {
// const { editorCtx, ...storedState } = state;
// localStorage.setItem('hooksIdeState', JSON.stringify(storedState))
// });

View File

@@ -0,0 +1,77 @@
import Router from "next/router";
import toast from "react-hot-toast";
import state, { FaucetAccountRes } from '../index';
export const names = [
"Alice",
"Bob",
"Carol",
"Carlos",
"Charlie",
"Dan",
"Dave",
"David",
"Faythe",
"Frank",
"Grace",
"Heidi",
"Judy",
"Olive",
"Peggy",
"Walter",
];
/* This function adds faucet account to application global state.
* It calls the /api/faucet endpoint which in send a HTTP POST to
* https://hooks-testnet.xrpl-labs.com/newcreds and it returns
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
* is protected with CORS so that's why we did our own endpoint
*/
export const addFaucetAccount = async (showToast: boolean = false) => {
// Lets limit the number of faucet accounts to 5 for now
if (state.accounts.length > 4) {
return toast.error("You can only have maximum 5 accounts");
}
if (typeof window !== 'undefined') {
const toastId = showToast ? toast.loading("Creating account") : "";
console.log(Router)
const res = await fetch(`${window.location.origin}/api/faucet`, {
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
if (showToast) {
return toast.error(json.error, { id: toastId });
} else {
return;
}
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
}
state.accounts.push({
name: names[state.accounts.length],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: 1,
hooks: [],
isLoading: false,
});
}
}
};
// fetch initial faucets
(async function fetchFaucets() {
if (typeof window !== 'undefined') {
if (state.accounts.length < 2) {
await addFaucetAccount();
setTimeout(() => {
addFaucetAccount();
}, 10000);
}
}
})();

View File

@@ -0,0 +1,90 @@
import toast from "react-hot-toast";
import Router from 'next/router';
import state from "../index";
import { saveFile } from "./saveFile";
import { decodeBinary } from "../../utils/decodeBinary";
import { ref } from "valtio";
/* compileCode sends the code of the active file to compile endpoint
* If all goes well you will get base64 encoded wasm file back with
* some extra logging information if we can provide it. This function
* also decodes the returned wasm and creates human readable WAT file
* out of it and store both in global state.
*/
export const compileCode = async (activeId: number) => {
// Save the file to global state
saveFile(false);
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error("Missing env!");
}
// Bail out if we're already compiling
if (state.compiling) {
// if compiling is ongoing return
return;
}
// Set loading state to true
state.compiling = true;
try {
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
output: "wasm",
compress: true,
files: [
{
type: "c",
name: state.files[activeId].name,
options: "-O0",
src: state.files[activeId].content,
},
],
}),
});
const json = await res.json();
state.compiling = false;
if (!json.success) {
state.logs.push({ type: "error", message: json.message });
if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => {
if (!task.success) {
state.logs.push({ type: "error", message: task?.console });
}
});
}
return toast.error(`Couldn't compile!`, { position: "bottom-center" });
}
state.logs.push({
type: "success",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy",
});
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
state.files[state.active].compiledContent = ref(bufferData);
// Import wabt from and create human readable version of wasm file and
// put it into state
import("wabt").then((wabt) => {
const ww = wabt.default();
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true,
});
myModule.applyNames();
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
state.files[state.active].compiledWatContent = wast;
toast.success("Compiled successfully!", { position: "bottom-center" });
});
} catch (err) {
console.log(err);
state.logs.push({
type: "error",
message: "Error occured while compiling!",
});
state.compiling = false;
}
};

View File

@@ -0,0 +1,8 @@
import state, { IFile } from '../index';
/* Initializes empty file to global state */
export const createNewFile = (name: string) => {
const emptyFile: IFile = { name, language: "c", content: "" };
state.files.push(emptyFile);
state.active = state.files.length - 1;
};

View File

@@ -0,0 +1,98 @@
import { derive, sign } from "xrpl-accountlib";
import state, { IAccount } from "../index";
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
if (!arrayBuffer) {
return "";
}
if (
typeof arrayBuffer !== "object" ||
arrayBuffer === null ||
typeof arrayBuffer.byteLength !== "number"
) {
throw new TypeError("Expected input to be an ArrayBuffer");
}
var view = new Uint8Array(arrayBuffer);
var result = "";
var value;
for (var i = 0; i < view.length; i++) {
value = view[i].toString(16);
result += value.length === 1 ? "0" + value : value;
}
return result;
}
/* deployHook function turns the wasm binary into
* hex string, signs the transaction and deploys it to
* Hooks testnet.
*/
export const deployHook = async (account: IAccount & { name?: string }) => {
if (
!state.files ||
state.files.length === 0 ||
!state.files?.[state.active]?.compiledContent
) {
return;
}
if (!state.files?.[state.active]?.compiledContent) {
return;
}
if (!state.client) {
return;
}
if (typeof window !== "undefined") {
const tx = {
Account: account.address,
TransactionType: "SetHook",
CreateCode: arrayBufferToHex(
state.files?.[state.active]?.compiledContent
).toUpperCase(),
HookOn: "0000000000000000",
Sequence: account.sequence,
Fee: "1000",
};
const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currentAccount) {
currentAccount.isLoading = true;
}
try {
const submitRes = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
if (submitRes.engine_result === "tesSUCCESS") {
state.deployLogs.push({
type: "success",
message: "Hook deployed successfully ✅",
});
state.deployLogs.push({
type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
});
} else {
state.deployLogs.push({
type: "error",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message}`,
});
}
} catch (err) {
console.log(err);
state.deployLogs.push({
type: "error",
message: "Error occured while deploying",
});
}
if (currentAccount) {
currentAccount.isLoading = false;
}
}
};

View File

@@ -0,0 +1,19 @@
import { createZip } from '../../utils/zip';
import { guessZipFileName } from '../../utils/helpers';
import state from '..'
import toast from 'react-hot-toast';
export const downloadAsZip = async () => {
try {
state.zipLoading = true
// TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content }));
const zipped = await createZip(files);
const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName);
} catch (error) {
toast.error('Error occured while creating zip file, try again later')
} finally {
state.zipLoading = false
}
};

View File

@@ -0,0 +1,57 @@
import { Octokit } from "@octokit/core";
import Router from "next/router";
import state from '../index';
const octokit = new Octokit();
/* Fetches Gist files from Githug Gists based on
* gistId and stores the content in global state
*/
export const fetchFiles = (gistId: string) => {
state.loading = true;
if (gistId && !state.files.length) {
state.logs.push({
type: "log",
message: `Fetching Gist with id: ${gistId}`,
});
octokit
.request("GET /gists/{gist_id}", { gist_id: gistId })
.then((res) => {
if (res.data.files && Object.keys(res.data.files).length > 0) {
const files = Object.keys(res.data.files).map((filename) => ({
name: res.data.files?.[filename]?.filename || "noname.c",
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
content: res.data.files?.[filename]?.content || "",
}));
state.loading = false;
if (files.length > 0) {
state.logs.push({
type: "success",
message: "Fetched successfully ✅",
});
state.files = files;
state.gistId = gistId;
state.gistName = Object.keys(res.data.files)?.[0] || "untitled";
state.gistOwner = res.data.owner?.login;
return;
} else {
// Open main modal if now files
state.mainModalOpen = true;
}
return Router.push({ pathname: "/develop" });
}
state.loading = false;
})
.catch((err) => {
state.loading = false;
state.logs.push({
type: "error",
message: `Couldn't find Gist with id: ${gistId}`,
});
return;
});
return;
}
state.loading = false;
};

View File

@@ -0,0 +1,29 @@
import toast from "react-hot-toast";
import { derive } from "xrpl-accountlib";
import state from '../index';
import { names } from './addFaucetAccount';
// Adds test account to global state with secret key
export const importAccount = (secret: string) => {
if (!secret) {
return toast.error("You need to add secret!");
}
if (state.accounts.find((acc) => acc.secret === secret)) {
return toast.error("Account already added!");
}
const account = derive.familySeed(secret);
if (!account.secret.familySeed) {
return toast.error(`Couldn't create account!`);
}
state.accounts.push({
name: names[state.accounts.length],
address: account.address || "",
secret: account.secret.familySeed || "",
xrp: "0",
sequence: 1,
hooks: [],
isLoading: false,
});
return toast.success("Account imported successfully!");
};

23
state/actions/index.ts Normal file
View File

@@ -0,0 +1,23 @@
import { addFaucetAccount } from "./addFaucetAccount";
import { compileCode } from "./compileCode";
import { createNewFile } from "./createNewFile";
import { deployHook } from "./deployHook";
import { fetchFiles } from "./fetchFiles";
import { importAccount } from "./importAccount";
import { saveFile } from "./saveFile";
import { syncToGist } from "./syncToGist";
import { updateEditorSettings } from "./updateEditorSettings";
import { downloadAsZip } from "./downloadAsZip";
export {
addFaucetAccount,
compileCode,
createNewFile,
deployHook,
fetchFiles,
importAccount,
saveFile,
syncToGist,
updateEditorSettings,
downloadAsZip
};

16
state/actions/saveFile.ts Normal file
View File

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

102
state/actions/syncToGist.ts Normal file
View File

@@ -0,0 +1,102 @@
import type { Session } from "next-auth";
import toast from "react-hot-toast";
import { Octokit } from "@octokit/core";
import Router from "next/router";
import state from '../index';
const octokit = new Octokit();
// Syncs the current files from the state to GitHub Gists.
export const syncToGist = async (
session?: Session | null,
createNewGist?: boolean
) => {
let files: Record<string, { filename: string; content: string }> = {};
state.gistLoading = true;
if (!session || !session.user) {
state.gistLoading = false;
return toast.error("You need to be logged in!");
}
const toastId = toast.loading("Pushing to Gist");
if (!state.files || !state.files.length) {
state.gistLoading = false;
return toast.error(`You need to create some files we can push to gist`, {
id: toastId,
});
}
if (
state.gistId &&
session?.user.username === state.gistOwner &&
!createNewGist
) {
// You can only remove files from Gist by updating file with empty contents
// So we need to fetch existing files and compare those to local state
// and then send empty content if we don't have matching files anymore
// on local state
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", {
gist_id: state.gistId,
});
if (currentFilesRes.data.files) {
Object.keys(currentFilesRes?.data?.files).forEach((filename) => {
files[`${filename}`] = { filename, content: "" };
});
}
state.files.forEach((file) => {
files[`${file.name}`] = { filename: file.name, content: file.content };
});
// Update existing Gist
octokit
.request("PATCH /gists/{gist_id}", {
gist_id: state.gistId,
files,
headers: {
authorization: `token ${session?.accessToken || ""}`,
},
})
.then((res) => {
state.gistLoading = false;
return toast.success("Updated to gist successfully!", { id: toastId });
})
.catch((err) => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not update Gist, try again later!`, {
id: toastId,
});
});
} else {
// Not Gist of the current user or it isn't Gist yet
state.files.forEach((file) => {
files[`${file.name}`] = { filename: file.name, content: file.content };
});
octokit
.request("POST /gists", {
files,
public: true,
headers: {
authorization: `token ${session?.accessToken || ""}`,
},
})
.then((res) => {
state.gistLoading = false;
state.gistOwner = res.data.owner?.login;
state.gistId = res.data.id;
state.gistName = Array.isArray(res.data.files)
? Object.keys(res.data?.files)?.[0]
: "Untitled";
Router.push({ pathname: `/develop/${res.data.id}` });
return toast.success("Created new gist successfully!", { id: toastId });
})
.catch((err) => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not create Gist, try again later!`, {
id: toastId,
});
});
}
};
export default syncToGist;

View File

@@ -0,0 +1,14 @@
import state, { IState } from '../index';
// Updates editor settings and stores them
// in global state
export const updateEditorSettings = (
editorSettings: IState["editorSettings"]
) => {
state.editorCtx?.getModels().forEach((model) => {
model.updateOptions({
...editorSettings,
});
});
return (state.editorSettings = editorSettings);
};

136
state/index.ts Normal file
View File

@@ -0,0 +1,136 @@
import { proxy, ref, subscribe } from "valtio";
import { devtools } from 'valtio/utils'
import type monaco from "monaco-editor";
import { XrplClient } from "xrpl-client";
export interface IFile {
name: string;
language: string;
content: string;
compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null;
}
export interface FaucetAccountRes {
address: string;
secret: string;
xrp: number;
hash: string;
code: string;
}
export interface IAccount {
name: string;
address: string;
secret: string;
xrp: string;
sequence: number;
hooks: string[];
isLoading: boolean;
}
export interface ILog {
type: "error" | "warning" | "log" | "success";
message: string;
link?: string;
linkText?: string;
}
export interface IState {
files: IFile[];
gistId?: string | null;
gistOwner?: string | null;
gistName?: string | null;
active: number;
activeWat: number;
loading: boolean;
gistLoading: boolean;
zipLoading: boolean;
compiling: boolean;
logs: ILog[];
deployLogs: ILog[];
editorCtx?: typeof monaco.editor;
editorSettings: {
tabSize: number;
};
client: XrplClient | null;
clientStatus: "offline" | "online";
mainModalOpen: boolean;
accounts: IAccount[];
}
// let localStorageState: null | string = null;
let initialState: IState = {
files: [],
active: 0,
activeWat: 0,
loading: false,
compiling: false,
logs: [],
deployLogs: [],
editorCtx: undefined,
gistId: undefined,
gistOwner: undefined,
gistName: undefined,
gistLoading: false,
zipLoading: false,
editorSettings: {
tabSize: 2,
},
client: null,
clientStatus: "offline" as "offline",
mainModalOpen: false,
accounts: [],
};
let localStorageAccounts: string | null = null;
let initialAccounts: IAccount[] = [];
// Check if there's a persited accounts in localStorage
if (typeof window !== "undefined") {
try {
localStorageAccounts = localStorage.getItem("hooksIdeAccounts");
} catch (err) {
console.log(`localStorage state broken`);
localStorage.removeItem("hooksIdeAccounts");
}
if (localStorageAccounts) {
initialAccounts = JSON.parse(localStorageAccounts);
}
}
// Initialize state
const state = proxy<IState>({
...initialState,
accounts: initialAccounts.length > 0 ? initialAccounts : [],
logs: [],
});
// Initialize socket connection
const client = new XrplClient("wss://hooks-testnet.xrpl-labs.com");
client.on("online", () => {
state.client = ref(client);
state.clientStatus = "online";
});
client.on("offline", () => {
state.clientStatus = "offline";
});
if (process.env.NODE_ENV !== "production") {
devtools(state, "Files State");
}
if (typeof window !== "undefined") {
subscribe(state, () => {
const { accounts, active } = state;
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
if (!state.files[active]?.compiledWatContent) {
state.activeWat = 0;
} else {
state.activeWat = active;
}
});
}
export default state

View File

@@ -11,6 +11,7 @@ import {
mauve,
pink,
yellow,
purple,
grayDark,
blueDark,
redDark,
@@ -20,6 +21,7 @@ import {
mauveDark,
pinkDark,
yellowDark,
purpleDark,
} from '@radix-ui/colors';
export const {
@@ -43,6 +45,7 @@ export const {
...mauve,
...pink,
...yellow,
...purple,
background: "$gray1",
text: "$gray12",
primary: "$plum",
@@ -52,10 +55,10 @@ export const {
fonts: {
body: 'Work Sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
heading: 'Work Sans, sans-serif',
monospace: 'Roboto, monospace',
monospace: 'Roboto Mono, monospace',
},
fontSizes: {
xs: "0.75rem",
xs: "0.6875rem",
sm: "0.875rem",
md: "1rem",
lg: "1.125rem",
@@ -298,7 +301,8 @@ export const darkTheme = createTheme('dark', {
...slateDark,
...mauveDark,
...pinkDark,
...yellowDark
...yellowDark,
...purpleDark,
},
});
@@ -312,4 +316,8 @@ export const globalStyles = globalCss({
'-webkit-font-smoothing': 'antialiased',
'-moz-osx-font-smoothing': 'grayscale'
},
'a': {
color: 'inherit',
textDecoration: 'none'
}
});

View File

@@ -8,11 +8,6 @@ body,
flex-direction: column;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

15
utils/decodeBinary.ts Normal file
View File

@@ -0,0 +1,15 @@
import { decodeRestrictedBase64ToBytes } from "./decodeRestrictedBase64ToBytes";
import { isZlibData, decompressZlib } from "./zlib";
import { fromByteArray } from "base64-js";
export async function decodeBinary(input: string): Promise<ArrayBuffer> {
let data = decodeRestrictedBase64ToBytes(input);
if (isZlibData(data)) {
data = await decompressZlib(data);
}
return data.buffer as ArrayBuffer;
}
export function encodeBinary(input: ArrayBuffer): string {
return fromByteArray(new Uint8Array(input));
}

View File

@@ -0,0 +1,44 @@
const base64DecodeMap = [ // starts at 0x2B
62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
0, 0, 0, 0, 0, 0, 0, // 0x3A-0x40
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, // 0x5B-0x0x60
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
44, 45, 46, 47, 48, 49, 50, 51
];
const base64DecodeMapOffset = 0x2B;
const base64EOF = 0x3D;
export function decodeRestrictedBase64ToBytes(encoded: string) {
let ch: any;
let code: any;
let code2: any;
const len = encoded.length;
const padding = encoded.charAt(len - 2) === "=" ? 2 : encoded.charAt(len - 1) === "=" ? 1 : 0;
const decoded = new Uint8Array((encoded.length >> 2) * 3 - padding);
for (let i = 0, j = 0; i < encoded.length;) {
ch = encoded.charCodeAt(i++);
code = base64DecodeMap[ch - base64DecodeMapOffset];
ch = encoded.charCodeAt(i++);
code2 = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = (code << 2) | ((code2 & 0x30) >> 4);
ch = encoded.charCodeAt(i++);
if (ch === base64EOF) {
return decoded;
}
code = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = ((code2 & 0x0f) << 4) | ((code & 0x3c) >> 2);
ch = encoded.charCodeAt(i++);
if (ch === base64EOF) {
return decoded;
}
code2 = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = ((code & 0x03) << 6) | code2;
}
return decoded;
}

9
utils/helpers.ts Normal file
View File

@@ -0,0 +1,9 @@
interface File {
name: string
}
export const guessZipFileName = (files: File[]) => {
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
parts = parts.length > 1 ? parts.slice(0, -1) : parts
return parts.join('')
}

50
utils/languageClient.ts Normal file
View File

@@ -0,0 +1,50 @@
import { MessageConnection } from "@codingame/monaco-jsonrpc";
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
import Router from "next/router";
import normalizeUrl from "normalize-url";
import ReconnectingWebSocket from "reconnecting-websocket";
export function createLanguageClient(connection: MessageConnection): MonacoLanguageClient {
return new MonacoLanguageClient({
name: "Clangd Language Client",
clientOptions: {
// use a language id as a document selector
documentSelector: ['c', 'h'],
// disable the default error handler
errorHandler: {
error: () => ErrorAction.Continue,
closed: () => {
if (Router.pathname.includes('/develop')) {
return CloseAction.Restart
} else {
return CloseAction.DoNotRestart
}
}
},
},
// create a language client connection from the JSON RPC connection on demand
connectionProvider: {
get: (errorHandler, closeHandler) => {
return Promise.resolve(createConnection(connection, errorHandler, closeHandler))
}
}
});
}
export function createUrl(path: string): string {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
return normalizeUrl(`${protocol}://${location.host}${location.pathname}${path}`);
}
export function createWebSocket(url: string) {
const socketOptions = {
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.3,
connectionTimeout: 10000,
maxRetries: Infinity,
debug: false
};
return new ReconnectingWebSocket(url, [], socketOptions);
}

59
utils/libwabt.js Normal file

File diff suppressed because one or more lines are too long

32
utils/zip.ts Normal file
View File

@@ -0,0 +1,32 @@
import JSZip, { JSZipFileOptions } from 'jszip'
import { saveAs } from 'file-saver'
interface File {
name: string
content: any
options?: JSZipFileOptions
}
interface Zipped {
saveFile: (filename: string) => void
data: Blob
}
export const createZip = async (files: File[]): Promise<Zipped> => {
const zip = new JSZip()
files.forEach(({ name, content, options }) => {
zip.file(name, content, options)
})
const data = await zip.generateAsync({ type: "blob" })
return {
saveFile: (filename: string) =>
saveAs(data, filename),
data
}
}

10
utils/zlib.ts Normal file
View File

@@ -0,0 +1,10 @@
export function isZlibData(data: Uint8Array): boolean {
// @ts-expect-error
const [firstByte, secondByte] = data;
return firstByte === 0x78 && (secondByte === 0x01 || secondByte === 0x9C || secondByte === 0xDA);
}
export async function decompressZlib(data: Uint8Array): Promise<Uint8Array> {
const { inflate } = await import("pako");
return inflate(data);
}

864
yarn.lock

File diff suppressed because it is too large Load Diff