Compare commits

...

63 Commits

Author SHA1 Message Date
muzam
17c67822a9 First draft of debug stream 2022-02-01 17:05:53 +05:30
muzam
0f15a85c45 Added additional tx type 2022-01-18 14:41:11 +05:30
muzam
0c4330e329 Support json fields and better error handling 2022-01-12 14:51:02 +05:30
muzam
a9676288ea implement reset transaction state 2022-01-11 20:20:39 +05:30
muzam
7354474c70 Implemented transactions ❤️‍🔥 2022-01-11 20:16:44 +05:30
muzam
ce5b307a8b Implement send transaction, payment works, yaay. 2022-01-10 15:21:59 +05:30
muzam
b28bcfdd0a Merge branch 'main' into test-page 2022-01-05 16:32:30 +05:30
muzam
7f06876e3e Test page UI layout 2022-01-05 16:32:07 +05:30
muzamil
fd479d8671 Merge pull request #41 from eqlabs/feat/templates
Fetch templates and header files according to selection.
2022-01-04 15:32:10 +05:30
Valtteri Karesto
938b567256 Merge pull request #46 from eqlabs/feat/patch-ripple-binary-codec
Feat/patch ripple binary codec
2021-12-23 09:08:38 +02:00
Valtteri Karesto
779f5aab0a Fixed typos 2021-12-22 16:14:35 +02:00
Valtteri Karesto
02194d8a98 Remove logs 2021-12-22 16:08:02 +02:00
Valtteri Karesto
5677fe34dc Add comments to state 2021-12-22 16:07:45 +02:00
Valtteri Karesto
895da89325 Add new version of ripple-binary-codec patch 2021-12-22 16:07:21 +02:00
Valtteri Karesto
b138cc8d5b Update readme 2021-12-22 16:06:52 +02:00
muzam
027b2c8ed4 remove console log 2021-12-21 17:07:45 +05:30
Valtteri Karesto
d85cc71817 Merge pull request #43 from eqlabs/feat/temporary-fix-for-editor
Roll back file paths for now
2021-12-21 11:18:58 +02:00
Valtteri Karesto
bac3522078 Roll back file paths for now 2021-12-21 11:07:52 +02:00
Vaclav Barta
b2c6aa7871 Merge pull request #42 from eqlabs/feature/workspace-location
fix for issue #39
2021-12-20 13:38:46 +01:00
Vaclav Barta
81e2a3673d fix for issue #39 2021-12-20 13:02:42 +01:00
muzam
b4ca360661 Implement read-only editors for some headers. 2021-12-16 22:30:03 +05:30
muzam
ad947be0bc Only fetch extra headers on template files. 2021-12-16 18:52:16 +05:30
muzam
f739d4da34 Fetch templates and header files according to selection. 2021-12-16 18:26:58 +05:30
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
57 changed files with 4642 additions and 693 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
@@ -24,6 +26,75 @@ You can start editing the page by modifying `pages/index.tsx`. The page auto-upd
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Github Login
If you want to use your Github app to provide login, here's the guide to do that.
- First go to https://github.com/settings/profile -> Developer Settings -> OAuth Apps
- Click "New OAuth App" button
- Give application some name eg. Xrpl Hooks Development
- Give some homepage url eg. localhost:3000
- Give some optional description (these values will show up on the popup when you login)
- Authorization callback URL should be http://localhost:3000/api/auth/callback (if you're creating the app for local development)
- Click register application
- Then a page should open up where you can get client id and client secret values. Copy paste those to .env.local to use them:
```
GITHUB_SECRET="client-secret-here"
GITHUB_ID="client-id-here"
```
Login should now work through your own Github OAuth app.
## Styling and Theming
This project uses Stitches (https://stitches.dev) for theming and styling the components. You should be quite familiar with the API if you have used for example styled-components earlier. Stitches should provide better performance, near zero runtime.
For components we try to use Radix-UI (https://www.radix-ui.com/) as much as possible. It may not provide all the necessary components so you're free to use other components/libraries if those makes sense. For colors we're using Radix-UI Colors (https://radix-ui.com/colors).
Theme file can be found under `./stitches.config.ts` file. When you're creating new components remeber to import `styled` from that file and not `@stitches` directly. That way it will provide the correct theme for you automatically.
Example:
```tsx
// Use our stitches.config instead of @stitches/react
import { styled } from "../stitches.config";
const Box = styled("div", {
boxSizing: "border-box",
});
export default Box;
```
Custom components can be found from `./components` folder.
## Monaco Editor
Project is relying on Monaco editor heavily. Instead of using Monaco editor directly we're using `@monaco-editor/react` which provides little helpers to use Monaco editor.
On the Develop page we're using following loader for Monaco editor:
```js
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
```
By default `@monaco-editor/react` was using 0.29.? version of Monaco editor. @codingame/monaco-languageclient library (connects to clangd language server) was using API methods that were introduced in Monaco Editor 0.30 so that's why we're loading certain version of it.
Monaco Languageclient related stuff is found from `./utils/languageClient.ts`. Basically we're connecting the editor to clangd language server which lives on separate backend. That project can be found from https://github.com/eqlabs/xrpl-hooks-compiler/. If you need access to that project ask permissions from @vbar (Vaclav Barta) on GitHub.
## Global state management
Global state management is handled with library called Valtio (https://github.com/pmndrs/valtio). Initial state can be found from `./state/index.ts` file. All the actions which updates the state is found under `./state/actions/` folder.
## Special notes
Since we are dealing with greenfield tech and one of the dependencies (ripple-binary-codec) doesn't yet support signing `SetHook` transactions we had to monkey patch the library with patch-package (https://www.npmjs.com/package/patch-package). We modified the definitions.json file of the ripple-binary-codec library and then ran `yarn patch-package ripple-binary-codec` which created `patches/ripple-binary-codec+1.2.0.patch` file to this project. This file contains the modifications to `ripple-binary-codec` library. package.json contains postinstall hook which runs patch-package, and it will add the patch on the file mentioned earlier. This happens automatically after running patch package.
## Learn More
To learn more about Next.js, take a look at the following resources:
@@ -32,9 +103,3 @@ To learn more about Next.js, take a look at the following resources:
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

471
components/Accounts.tsx Normal file
View File

@@ -0,0 +1,471 @@
import toast from "react-hot-toast";
import { useSnapshot } from "valtio";
import { ArrowSquareOut, Copy, Wallet, X } from "phosphor-react";
import React, { useEffect, useState, FC } 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, Heading, Stack, Text, Flex } from ".";
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>
);
};
interface AccountProps {
card?: boolean;
hideDeployBtn?: boolean;
showHookStats?: boolean;
}
const Accounts: FC<AccountProps> = props => {
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",
backgroundColor: props.card ? "$deep" : "$mauve1",
position: "relative",
width: "100%",
height: "100%",
flexShrink: 0,
borderTop: "1px solid $mauve6",
borderRight: "1px solid $mauve6",
borderLeft: "1px solid $mauve6",
borderBottom: "1px solid $mauve6",
borderRadius: props.card ? "$md" : undefined,
}}
>
<Container css={{ p: 0, flexShrink: 1, height: "100%" }}>
<Flex css={{ py: "$3", borderBottom: props.card ? "1px solid $mauve6" : undefined }}>
<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="sm" onClick={() => addFaucetAccount(true)}>
Create
</Button>
<ImportAccountDialog />
</Flex>
</Flex>
<Stack
css={{
flexDirection: "column",
width: "100%",
fontSize: "13px",
wordWrap: "break-word",
fontWeight: "$body",
fontFamily: "$monospace",
gap: 0,
height: "calc(100% - 52px)",
flexWrap: "nowrap",
overflowY: "auto",
}}
>
{snap.accounts.map(account => (
<Flex
column
key={account.address + account.name}
onClick={() => setActiveAccountAddress(account.address)}
css={{
px: "$3",
py: props.card ? "$3" : "$2",
cursor: "pointer",
borderBottom: props.card ? "1px solid $mauve6" : undefined,
"@hover": {
"&:hover": {
background: "$backgroundAlt",
},
},
}}
>
<Flex
row
css={{
justifyContent: "space-between",
}}
>
<Box>
<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>
</Box>
{!props.hideDeployBtn && (
<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>
{props.showHookStats && (
<Text muted small css={{ mt: "$2" }}>
X hooks installed
</Text>
)}
</Flex>
))}
</Stack>
</Container>
<AccountDialog
activeAccountAddress={activeAccountAddress}
setActiveAccountAddress={setActiveAccountAddress}
/>
</Box>
);
};
const ImportAccountDialog = () => {
const [value, setValue] = useState("");
return (
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm">
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",

View File

@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";
import { useSnapshot } from "valtio";
import { Select } from ".";
import state from "../state";
import LogBox from "./LogBox";
import Text from "./Text";
const DebugStream = () => {
const snap = useSnapshot(state);
const accountOptions = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState<typeof accountOptions[0] | null>(null);
const renderNav = () => (
<>
<Text css={{ mx: "$2", fontSize: "inherit" }}>Account: </Text>
<Select
instanceId="debugStreamAccount"
placeholder="Select account"
options={accountOptions}
hideSelectedOptions
value={selectedAccount}
onChange={acc => setSelectedAccount(acc as any)}
css={{ width: "30%" }}
/>
</>
);
useEffect(() => {
const account = selectedAccount?.value;
if (!account) {
return;
}
const socket = new WebSocket(`wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`);
const onOpen = () => {
state.debugLogs = [];
state.debugLogs.push({
type: "success",
message: `Debug stream opened for account ${account}`,
});
};
const onError = () => {
state.debugLogs.push({
type: "error",
message: "Something went wrong in establishing connection!",
});
setSelectedAccount(null);
};
const onMessage = (event: any) => {
if (!event.data) return;
state.debugLogs.push({
type: "log",
message: event.data,
});
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onError);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onError);
socket.removeEventListener("message", onMessage);
socket.close();
};
}, [selectedAccount]);
return (
<LogBox
enhanced
renderNav={renderNav}
title="Debug stream"
logs={snap.debugLogs}
clearLog={() => (state.debugLogs = [])}
/>
);
};
export default DebugStream;

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

@@ -3,6 +3,23 @@ import Box from "./Box";
const Flex = styled(Box, {
display: "flex",
variants: {
row: {
true: {
flexDirection: "row",
},
},
column: {
true: {
flexDirection: "column",
},
},
fluid: {
true: {
width: '100%'
}
}
},
});
export default Flex;

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,48 @@ 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 { apiHeaderFiles } from "../state/constants";
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 validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find(h => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true });
} else {
editor.updateOptions({ readOnly: false });
}
};
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(() => {
if (editorRef.current) validateWritability(editorRef.current);
}, [snap.active]);
useEffect(() => {
return () => {
subscriptionRef?.current?.close();
};
}, []);
return (
<Box
css={{
@@ -35,12 +67,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) => {
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,19 +133,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,
() => {
saveFile(editor.getValue());
}
);
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
saveFile();
});
validateWritability(editor)
}}
theme={theme === "dark" ? "dark" : "light"}
/>
@@ -81,9 +160,7 @@ const HooksEditor = () => {
<Box css={{ display: "inline-flex", pl: "35px" }}>
<ArrowBendLeftUp size={30} />
</Box>
<Box
css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}
>
<Box css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}>
<Text
css={{
fontSize: "14px",

View File

@@ -110,20 +110,22 @@ export const Input = styled("input", {
backgroundColor: "transparent",
},
},
deep: {
backgroundColor: "$deep",
boxShadow: "none",
},
},
state: {
invalid: {
boxShadow: "inset 0 0 0 1px $colors$red7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$red8, 0px 0px 0px 1px $colors$red8",
boxShadow: "inset 0px 0px 0px 1px $colors$red8, 0px 0px 0px 1px $colors$red8",
},
},
valid: {
boxShadow: "inset 0 0 0 1px $colors$green7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$green8, 0px 0px 0px 1px $colors$green8",
boxShadow: "inset 0px 0px 0px 1px $colors$green8, 0px 0px 0px 1px $colors$green8",
},
},
},

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;

119
components/LogBox.tsx Normal file
View File

@@ -0,0 +1,119 @@
import React, { useRef, useLayoutEffect, ReactNode } 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[];
renderNav?: () => ReactNode;
enhanced?: boolean;
}
const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhanced }) => {
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", alignItems: "center", fontSize: "$sm", fontWeight: 300 }}>
<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>
{renderNav?.()}
<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}
css={{
"@hover": {
"&:hover": {
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
},
},
p: "$2 $1",
}}
>
<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

@@ -4,6 +4,7 @@ const Text = styled("span", {
fontFamily: "$monospace",
lineHeight: "$body",
color: "$text",
wordWrap: 'break-word',
variants: {
variant: {
log: {
@@ -15,6 +16,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";
@@ -27,6 +27,7 @@ import {
DialogTrigger,
} from "./Dialog";
import PanelBox from "./PanelBox";
import { templateFileIds } from '../state/constants';
const Navigation = () => {
const router = useRouter();
@@ -82,12 +83,8 @@ const Navigation = () => {
<Spinner />
) : (
<>
<Heading css={{ lineHeight: 1 }}>
{snap.files?.[0]?.name || "XRPL Hooks"}
</Heading>
<Text
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
>
<Heading css={{ lineHeight: 1 }}>{snap.files?.[0]?.name || "XRPL Hooks"}</Heading>
<Text css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}>
{snap.files.length > 0 ? "Gist: " : "Playground"}
<Text css={{ color: "$mauve12" }}>
{snap.files.length > 0 &&
@@ -97,217 +94,203 @@ 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,
}}
>
<Flex
{router.isReady && (
<ButtonGroup css={{ marginLeft: "auto" }}>
<Dialog open={snap.mainModalOpen} onOpenChange={open => (state.mainModalOpen = open)}>
<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/${templateFileIds.starter}`}>
<Heading>Starter</Heading>
<Text>Just an empty starter with essential imports</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.starter}`}>
<Heading>Firewall</Heading>
<Text>This Hook essentially checks a blacklist of accounts</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.accept}`}>
<Heading>Accept</Heading>
<Text>This hook just accepts any transaction coming through it</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.notary}`}>
<Heading>Notary</Heading>
<Text>Collecting signatures for multi-sign transactions</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.carbon}`}>
<Heading>Carbon</Heading>
<Text>Send a percentage of sum to an address</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.peggy}`}>
<Heading>Peggy</Heading>
<Text>An oracle based stabe coin hook</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={{
@@ -330,42 +313,18 @@ const Navigation = () => {
}}
>
<ButtonGroup>
<Link
href={gistId ? `/develop/${gistId}` : "/develop"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
>
<Link href={gistId ? `/develop/${gistId}` : "/develop"} passHref shallow>
<Button as="a" outline={!router.pathname.includes("/develop")} uppercase>
Develop
</Button>
</Link>
<Link
href={gistId ? `/deploy/${gistId}` : "/deploy"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
>
<Link href={gistId ? `/deploy/${gistId}` : "/deploy"} passHref shallow>
<Button as="a" outline={!router.pathname.includes("/deploy")} uppercase>
Deploy
</Button>
</Link>
<Link
href={gistId ? `/test/${gistId}` : "/test"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
>
<Link href={gistId ? `/test/${gistId}` : "/test"} passHref shallow>
<Button as="a" outline={!router.pathname.includes("/test")} uppercase>
Test
</Button>
</Link>

56
components/Select.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { FC } from "react";
import { gray, grayDark } from "@radix-ui/colors";
import { useTheme } from "next-themes";
import { styled } from '../stitches.config';
import dynamic from 'next/dynamic';
import type { Props } from "react-select";
const SelectInput = dynamic(() => import("react-select"), { ssr: false });
const Select: FC<Props> = props => {
const { theme } = useTheme();
const isDark = theme === "dark";
const colors: any = {
// primary: pink.pink9,
primary: isDark ? grayDark.gray4 : gray.gray4,
secondary: isDark ? grayDark.gray8 : gray.gray8,
background: isDark ? "rgb(10, 10, 10)" : "rgb(244, 244, 244)",
searchText: isDark ? grayDark.gray12 : gray.gray12,
placeholder: isDark ? grayDark.gray11 : gray.gray11,
};
colors.outline = colors.background;
colors.selected = colors.secondary;
return (
<SelectInput
menuPosition="fixed"
theme={theme => ({
...theme,
spacing: {
...theme.spacing,
controlHeight: 30
},
colors: {
primary: colors.selected,
primary25: colors.primary,
primary50: colors.primary,
primary75: colors.primary,
danger: colors.primary,
dangerLight: colors.primary,
neutral0: colors.background,
neutral5: colors.primary,
neutral10: colors.primary,
neutral20: colors.outline,
neutral30: colors.primary,
neutral40: colors.primary,
neutral50: colors.placeholder,
neutral60: colors.primary,
neutral70: colors.primary,
neutral80: colors.searchText,
neutral90: colors.primary,
},
})}
{...props}
/>
);
};
export default styled(Select, {});

70
components/Tabs.tsx Normal file
View File

@@ -0,0 +1,70 @@
import { useEffect, useState } from "react";
import type { ReactNode, ReactElement } from "react";
import { Button, Stack } from ".";
interface TabProps {
header?: string;
children: ReactNode;
}
interface Props {
activeIndex?: number;
activeHeader?: string;
headless?: boolean;
children: ReactElement<TabProps>[];
}
export const Tab = (props: TabProps) => null;
export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) => {
const [active, setActive] = useState(activeIndex || 0);
const tabProps: TabProps[] = children.map(elem => elem.props);
useEffect(() => {
if (activeIndex) setActive(activeIndex);
}, [activeIndex]);
useEffect(() => {
if (activeHeader) {
const idx = tabProps.findIndex(tab => tab.header === activeHeader);
setActive(idx);
}
}, [activeHeader, tabProps]);
return (
<>
{!headless && (
<Stack
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
}}
>
{tabProps.map((prop, idx) => (
<Button
key={prop.header}
role="tab"
tabIndex={idx}
onClick={() => setActive(idx)}
onKeyPress={() => setActive(idx)}
outline={active !== idx}
size="sm"
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{prop.header || idx}
</Button>
))}
</Stack>
)}
{tabProps[active].children}
</>
);
};

View File

@@ -4,6 +4,18 @@ const Text = styled("span", {
fontFamily: "$body",
lineHeight: "$body",
color: "$text",
variants: {
small: {
true: {
fontSize: '$xs'
}
},
muted: {
true: {
color: '$mauve9'
}
}
}
});
export default Text;

15
components/index.tsx Normal file
View File

@@ -0,0 +1,15 @@
export { default as Flex } from './Flex'
export { default as Container } from './Container'
export { default as Heading } from './Heading'
export { default as Stack } from './Stack'
export { default as Text } from './Text'
export { default as Input } from './Input'
export { default as Select } from './Select'
export * from './Tabs'
export * from './AlertDialog'
export { default as Box } from './Box'
export { default as Button } from './Button'
export { default as ButtonGroup } from './ButtonGroup'
export { default as DeployFooter } from './DeployFooter'
export * from './Dialog'
export * from './DropdownMenu'

221
content/transactions.json Normal file
View File

@@ -0,0 +1,221 @@
[
{
"TransactionType": "AccountDelete",
"Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
"DestinationTag": 13,
"Fee": "2000000",
"Sequence": 2470665,
"Flags": 2147483648
},
{
"TransactionType": "AccountSet",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12",
"Sequence": 5,
"Domain": "6578616D706C652E636F6D",
"SetFlag": 5,
"MessageKey": "03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB"
},
{
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"TransactionType": "CheckCancel",
"CheckID": "49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0",
"Fee": "12"
},
{
"Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"TransactionType": "CheckCash",
"Amount": {
"value": "100",
"type": "currency"
},
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12"
},
{
"TransactionType": "CheckCreate",
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"SendMax": "100000000",
"Expiration": 570113521,
"InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
"DestinationTag": 1,
"Fee": "12"
},
{
"TransactionType": "DepositPreauth",
"Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
"Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
"Fee": "10",
"Flags": 2147483648,
"Sequence": 2
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCreate",
"Amount": {
"value": "100",
"type": "currency"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958,
"FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"DestinationTag": 23480,
"SourceTag": 11747
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowFinish",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000"
},
{
"TransactionType": "NFTokenBurn",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
},
{
"TransactionType": "NFTokenAcceptOffer",
"Fee": "10"
},
{
"TransactionType": "NFTokenCancelOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
},
{
"TransactionType": "NFTokenCreateOffer",
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": {
"value": "100",
"type": "currency"
},
"Flags": 1
},
{
"TransactionType": "OfferCancel",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": 0,
"LastLedgerSequence": 7108629,
"OfferSequence": 6,
"Sequence": 7
},
{
"TransactionType": "OfferCreate",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": 0,
"LastLedgerSequence": 7108682,
"Sequence": 8,
"TakerGets": "6000000",
"Amount": {
"value": "100",
"type": "currency"
}
},
{
"TransactionType": "Payment",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Amount": {
"value": "100",
"type": "currency"
},
"Fee": "12",
"Flags": 2147483648,
"Sequence": 2
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelCreate",
"Amount": {
"value": "100",
"type": "currency"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400,
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558,
"DestinationTag": 23480,
"SourceTag": 11747
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelFund",
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": {
"value": "200",
"type": "currency"
},
"Expiration": 543171558
},
{
"Flags": 0,
"TransactionType": "SetRegularKey",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12",
"RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
},
{
"Flags": 0,
"TransactionType": "SignerListSet",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12",
"SignerQuorum": 3,
"SignerEntries": {
"type": "json",
"value": [
{
"SignerEntry": {
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SignerWeight": 2
}
},
{
"SignerEntry": {
"Account": "rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v",
"SignerWeight": 1
}
},
{
"SignerEntry": {
"Account": "raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n",
"SignerWeight": 1
}
}
]
}
},
{
"TransactionType": "TicketCreate",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "10",
"Sequence": 381,
"TicketCount": 10
},
{
"TransactionType": "TrustSet",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": 262144,
"LastLedgerSequence": 8007750,
"Amount": {
"value": "100",
"type": "currency"
},
"Sequence": 12
}
]

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,40 @@
"@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",
"base64-js": "^1.5.1",
"dinero.js": "^1.9.1",
"file-saver": "^2.0.5",
"jszip": "^3.7.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-select": "^5.2.1",
"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/dinero.js": "^1.9.0",
"@types/file-saver": "^2.0.4",
"@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,20 @@ 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 +52,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,41 @@
import Container from "../../components/Container";
import React from "react";
import dynamic from "next/dynamic";
import { Flex, Box } from "../../components";
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, height: 'calc(100vh - 30vh - 60px)' }}>
<DeployEditor />
</main>
<Flex css={{ flexDirection: "row", width: "100%", minHeight: '225px', height: '30vh' }}>
<Box css={{ width: "100%" }}>
<Accounts />
</Box>
<Box css={{ width: "100%" }}>
<LogBox
title="Deploy Log"
logs={snap.deployLogs}
clearLog={() => (state.deployLogs = [])}
/>
</Box>
</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

@@ -1,7 +1,326 @@
import Container from "../../components/Container";
import { Container, Flex, Box, Tabs, Tab, Input, Select, Text, Button } from "../../components";
import { Play } from "phosphor-react";
import dynamic from "next/dynamic";
import { useSnapshot } from "valtio";
import state from "../../state";
import { sendTransaction } from "../../state/actions";
import { useCallback, useEffect, useState } from "react";
import transactionsData from "../../content/transactions.json";
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false,
});
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Accounts = dynamic(() => import("../../components/Accounts"), {
ssr: false,
});
// type SelectOption<T> = { value: T, label: string };
type TxFields = Omit<typeof transactionsData[0], "Account" | "Sequence" | "TransactionType">;
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
const Transaction = () => {
const snap = useSnapshot(state);
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const [selectedTransaction, setSelectedTransaction] = useState<
typeof transactionsOptions[0] | null
>(null);
const accountOptions = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState<typeof accountOptions[0] | null>(null);
const destAccountOptions = snap.accounts
.map(acc => ({
label: acc.name,
value: acc.address,
}))
.filter(acc => acc.value !== selectedAccount?.value);
const [selectedDestAccount, setSelectedDestAccount] = useState<
typeof destAccountOptions[0] | null
>(null);
const [txIsLoading, setTxIsLoading] = useState(false);
const [txIsDisabled, setTxIsDisabled] = useState(false);
const [txFields, setTxFields] = useState<TxFields>({});
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = snap.accounts.find(acc => acc.address === selectedAccount?.value);
if (!account || !transactionType || txIsLoading) {
setTxIsDisabled(true);
} else {
setTxIsDisabled(false);
}
}, [txIsLoading, selectedTransaction, selectedAccount, snap.accounts]);
useEffect(() => {
let _txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === selectedTransaction?.value
);
if (!_txFields) return setTxFields({});
_txFields = { ..._txFields } as TxFields;
setSelectedDestAccount(null);
// @ts-ignore
delete _txFields.TransactionType;
// @ts-ignore
delete _txFields.Account;
// @ts-ignore
delete _txFields.Sequence;
setTxFields(_txFields);
}, [selectedTransaction, setSelectedDestAccount]);
const submitTest = useCallback(async () => {
const account = snap.accounts.find(acc => acc.address === selectedAccount?.value);
const TransactionType = selectedTransaction?.value;
if (!account || !TransactionType || txIsDisabled) return;
setTxIsLoading(true);
// setTxIsError(null)
try {
let options = { ...txFields };
options.Destination = selectedDestAccount?.value;
(Object.keys(options) as (keyof TxFields)[]).forEach(field => {
let _value = options[field];
// convert currency
if (typeof _value === "object" && _value.type === "currency") {
if (+_value.value) {
options[field] = (+_value.value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (typeof _value === "object" && _value.type === "json") {
if (typeof _value.value === "object") {
options[field] = _value.value as any;
} else {
try {
options[field] = JSON.parse(_value.value);
} catch (error) {
const message = `Input error for json field '${field}': ${
error instanceof Error ? error.message : ""
}`;
throw Error(message);
}
}
}
// delete unneccesary fields
if (!options[field]) {
delete options[field];
}
});
await sendTransaction(account, {
TransactionType,
...options,
});
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({ type: "error", message: error.message });
}
}
setTxIsLoading(false);
}, [
selectedAccount,
selectedDestAccount,
selectedTransaction,
snap.accounts,
txFields,
txIsDisabled,
]);
const resetState = useCallback(() => {
setSelectedAccount(null);
setSelectedDestAccount(null);
setSelectedTransaction(null);
setTxFields({});
setTxIsDisabled(false);
setTxIsLoading(false);
}, []);
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(k => !usualFields.includes(k)) as OtherFields;
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }}>
<Container css={{ p: "$3 0", fontSize: "$sm", height: "calc(100% - 28px)" }}>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={tt => setSelectedTransaction(tt as any)}
/>
</Flex>
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={acc => setSelectedAccount(acc as any)}
/>
</Flex>
{txFields.Amount !== undefined && (
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={e =>
setTxFields({
...txFields,
Amount: { type: "currency", value: e.target.value },
})
}
variant="deep"
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={acc => setSelectedDestAccount(acc as any)}
/>
</Flex>
)}
{otherFields.map(field => {
let _value = txFields[field];
let value = typeof _value === "object" ? _value.value : _value;
value = typeof value === "object" ? JSON.stringify(value) : value?.toLocaleString();
let isCurrency = typeof _value === "object" && _value.type === "currency";
return (
<Flex
key={field}
row
fluid
css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}
>
<Text muted css={{ mr: "$3" }}>
{field + (isCurrency ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e =>
setTxFields({
...txFields,
[field]:
typeof _value === "object"
? { ..._value, value: e.target.value }
: e.target.value,
})
}
variant="deep"
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
);
})}
</Flex>
</Container>
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
}}
>
<Button outline>VIEW AS JSON</Button>
<Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
);
};
const Test = () => {
return <Container css={{ py: "$10" }}>This will be the test page</Container>;
const snap = useSnapshot(state);
return (
<Container css={{ py: "$3", px: 0 }}>
<Flex
row
fluid
css={{ justifyContent: "center", mb: "$2", height: "40vh", minHeight: "300px", p: "$3 $2" }}
>
<Box css={{ width: "60%", px: "$2", maxWidth: "800px", height: "100%", overflow: "auto" }}>
<Tabs>
{/* TODO Dynamic tabs */}
<Tab header="test1.json">
<Transaction />
</Tab>
<Tab header="test2.json">
<Transaction />
</Tab>
</Tabs>
</Box>
<Box css={{ width: "40%", mx: "$2", height: "100%", maxWidth: "750px" }}>
<Accounts card hideDeployBtn showHookStats />
</Box>
</Flex>
<Flex row fluid css={{ borderBottom: "1px solid $mauve8" }}>
<Box css={{ width: "50%", borderRight: "1px solid $mauve8" }}>
<LogBox
title="Development Log"
logs={snap.transactionLogs}
clearLog={() => (state.transactionLogs = [])}
/>
</Box>
<Box css={{ width: "50%" }}>
<DebugStream />
</Box>
</Flex>
</Container>
);
};
export default Test;

View File

@@ -0,0 +1,419 @@
diff --git a/node_modules/ripple-binary-codec/dist/enums/definitions.json b/node_modules/ripple-binary-codec/dist/enums/definitions.json
index 2333c42..b8f8eab 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,9 +41,7 @@
"Check": 67,
"Nickname": 110,
"Contract": 99,
- "NFTokenPage": 80,
- "NFTokenOffer": 55,
- "NegativeUNL": 78
+ "GeneratorMap": 103
},
"FIELDS": [
[
@@ -95,16 +94,6 @@
"type": "UInt16"
}
],
- [
- "TransferFee",
- {
- "nth": 4,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "UInt16"
- }
- ],
[
"Flags",
{
@@ -455,6 +444,16 @@
"type": "UInt32"
}
],
+ [
+ "EmitGeneration",
+ {
+ "nth": 43,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "UInt32"
+ }
+ ],
[
"IndexNext",
{
@@ -635,16 +634,6 @@
"type": "Hash256"
}
],
- [
- "TokenID",
- {
- "nth": 10,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Hash256"
- }
- ],
[
"BookDirectory",
{
@@ -916,7 +905,7 @@
}
],
[
- "URI",
+ "Generator",
{
"nth": 5,
"isVLEncoded": true,
@@ -1045,36 +1034,6 @@
"type": "Blob"
}
],
- [
- "UNLModifyValidator",
- {
- "nth": 19,
- "isVLEncoded": true,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Blob"
- }
- ],
- [
- "ValidatorToDisable",
- {
- "nth": 20,
- "isVLEncoded": true,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Blob"
- }
- ],
- [
- "ValidatorToReEnable",
- {
- "nth": 21,
- "isVLEncoded": true,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Blob"
- }
- ],
[
"Account",
{
@@ -1156,7 +1115,7 @@
}
],
[
- "Minter",
+ "EmitCallback",
{
"nth": 9,
"isVLEncoded": true,
@@ -1276,9 +1235,9 @@
}
],
[
- "NonFungibleToken",
+ "Signer",
{
- "nth": 12,
+ "nth": 16,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1286,9 +1245,9 @@
}
],
[
- "Signer",
+ "Majority",
{
- "nth": 16,
+ "nth": 18,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1296,9 +1255,9 @@
}
],
[
- "Majority",
+ "DisabledValidator",
{
- "nth": 18,
+ "nth": 19,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1306,9 +1265,9 @@
}
],
[
- "DisabledValidator",
+ "EmitDetails",
{
- "nth": 19,
+ "nth": 12,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1395,16 +1354,6 @@
"type": "STArray"
}
],
- [
- "NonFungibleTokens",
- {
- "nth": 10,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "STArray"
- }
- ],
[
"Majorities",
{
@@ -1415,16 +1364,6 @@
"type": "STArray"
}
],
- [
- "DisabledValidators",
- {
- "nth": 17,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "STArray"
- }
- ],
[
"CloseResolution",
{
@@ -1535,16 +1474,6 @@
"type": "Vector256"
}
],
- [
- "TokenIDs",
- {
- "nth": 4,
- "isVLEncoded": true,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Vector256"
- }
- ],
[
"Transaction",
{
@@ -1596,7 +1525,7 @@
}
],
[
- "TicketCount",
+ "HookStateCount",
{
"nth": 40,
"isVLEncoded": false,
@@ -1606,7 +1535,7 @@
}
],
[
- "TicketSequence",
+ "HookReserveCount",
{
"nth": 41,
"isVLEncoded": false,
@@ -1616,7 +1545,7 @@
}
],
[
- "TokenTaxon",
+ "HookDataMaxSize",
{
"nth": 42,
"isVLEncoded": false,
@@ -1626,23 +1555,23 @@
}
],
[
- "MintedTokens",
+ "HookOn",
{
- "nth": 43,
+ "nth": 16,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
- "type": "UInt32"
+ "type": "UInt64"
}
],
[
- "BurnedTokens",
+ "EmitBurden",
{
- "nth": 44,
+ "nth": 12,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
- "type": "UInt32"
+ "type": "UInt64"
}
],
[
@@ -1686,29 +1615,9 @@
}
],
[
- "PreviousPageMin",
+ "EmitParentTxnID",
{
- "nth": 26,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Hash256"
- }
- ],
- [
- "NextPageMin",
- {
- "nth": 27,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "Hash256"
- }
- ],
- [
- "BuyOffer",
- {
- "nth": 28,
+ "nth": 10,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1716,9 +1625,9 @@
}
],
[
- "SellOffer",
+ "EmitNonce",
{
- "nth": 29,
+ "nth": 11,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
@@ -1735,16 +1644,6 @@
"type": "UInt8"
}
],
- [
- "UNLModifyDisabling",
- {
- "nth": 17,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "UInt8"
- }
- ],
[
"DestinationNode",
{
@@ -1754,36 +1653,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 +1777,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,13 +1804,11 @@
"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
+ "SetFee": 101
}
}

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,76 @@
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") : "";
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,71 @@
import { Octokit } from "@octokit/core";
import Router from "next/router";
import state from '../index';
import { templateFileIds } from '../constants';
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 (!Object.values(templateFileIds).includes(gistId)) {
return res
}
// in case of templates, fetch header file(s) and append to res
return octokit.request("GET /gists/{gist_id}", { gist_id: templateFileIds.headers }).then(({ data: { files: headerFiles } }) => {
const files = { ...res.data.files, ...headerFiles }
res.data.files = files
return res
})
})
.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) => {
// console.error(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!");
};

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

@@ -0,0 +1,25 @@
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";
import { sendTransaction } from "./sendTransaction";
export {
addFaucetAccount,
compileCode,
createNewFile,
deployHook,
fetchFiles,
importAccount,
saveFile,
syncToGist,
updateEditorSettings,
downloadAsZip,
sendTransaction
};

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" });
}
};

View File

@@ -0,0 +1,50 @@
import { derive, sign } from "xrpl-accountlib";
import state from '..'
import type { IAccount } from "..";
interface TransactionOptions {
TransactionType: string,
Account?: string,
Fee?: string,
Destination?: string
[index: string]: any
}
export const sendTransaction = async (account: IAccount, txOptions: TransactionOptions) => {
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = "1000", ...opts } = txOptions
const tx: TransactionOptions = {
Account: account.address,
Sequence: account.sequence, // TODO auto-fillable
Fee, // TODO auto-fillable
...opts
};
console.log({ tx });
try {
const signedAccount = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, signedAccount);
const response = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
if (response.engine_result === "tesSUCCESS") {
state.transactionLogs.push({
type: 'success',
message: `Transaction success [${response.engine_result}]: ${response.engine_result_message}`
})
} else {
state.transactionLogs.push({
type: "error",
message: `[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
});
}
} catch (err) {
console.error(err);
state.transactionLogs.push({
type: "error",
message: err instanceof Error ? `Error: ${err.message}` : 'Something went wrong, try again later',
});
}
};

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);
};

1
state/constants/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './templates'

View File

@@ -0,0 +1,11 @@
export const templateFileIds = {
'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept
'accept': '1d14e51e2e02dc0a508cb0733767a914',
'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26',
'notary': 'a789c75f591eeab7932fd702ed8cf9ea',
'carbon': '43925143fa19735d8c6505c34d3a6a47',
'peggy': 'ceaf352e2a65741341033ab7ef05c448',
'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
}
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'hookmacro.h']

142
state/index.ts Normal file
View File

@@ -0,0 +1,142 @@
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[];
transactionLogs: ILog[];
debugLogs: 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 file index on the Develop page editor
active: 0,
// Active file index on the Deploy page editor
activeWat: 0,
loading: false,
compiling: false,
logs: [],
deployLogs: [],
transactionLogs: [],
debugLogs: [],
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

@@ -1,6 +1,7 @@
// stitches.config.ts
import type Stitches from '@stitches/react';
import { createStitches } from '@stitches/react';
import {
gray,
blue,
@@ -11,6 +12,7 @@ import {
mauve,
pink,
yellow,
purple,
grayDark,
blueDark,
redDark,
@@ -20,6 +22,7 @@ import {
mauveDark,
pinkDark,
yellowDark,
purpleDark,
} from '@radix-ui/colors';
export const {
@@ -43,19 +46,22 @@ export const {
...mauve,
...pink,
...yellow,
...purple,
background: "$gray1",
backgroundAlt: "$gray4",
text: "$gray12",
primary: "$plum",
white: "white",
black: "black"
black: "black",
'deep': 'rgb(244, 244, 244)'
},
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 +304,10 @@ export const darkTheme = createTheme('dark', {
...slateDark,
...mauveDark,
...pinkDark,
...yellowDark
...yellowDark,
...purpleDark,
deep: 'rgb(10, 10, 10)',
// backgroundA: transparentize(0.1, grayDark.gray1),
},
});
@@ -312,4 +321,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);
}

1011
yarn.lock

File diff suppressed because it is too large Load Diff