Compare commits

..

1 Commits

Author SHA1 Message Date
Vaclav Barta
3ed97d2873 requiring user-quoted Hook Parameter values 2022-04-19 12:37:21 +02:00
65 changed files with 1726 additions and 4241 deletions

View File

@@ -1,5 +1,4 @@
NEXTAUTH_URL=https://example.com NEXTAUTH_URL=https://example.com
NEXTAUTH_SECRET="1234"
GITHUB_SECRET="" GITHUB_SECRET=""
GITHUB_ID="" GITHUB_ID=""
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build" NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"

View File

@@ -1,8 +1,6 @@
# XRPL Hooks Builder # XRPL Hooks IDE
https://hooks-builder.xrpl.org/ This is the repository for XRPL Hooks IDE. This project is built with Next.JS
This is the repository for XRPL Hooks Builder. This project is built with Next.JS
## General ## General
@@ -108,5 +106,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. - [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! You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

@@ -18,7 +18,7 @@ import {
DialogTrigger, DialogTrigger,
} from "./Dialog"; } from "./Dialog";
import { css } from "../stitches.config"; import { css } from "../stitches.config";
import { Input, Label } from "./Input"; import { Input } from "./Input";
import truncate from "../utils/truncate"; import truncate from "../utils/truncate";
const labelStyle = css({ const labelStyle = css({
@@ -116,16 +116,9 @@ export const AccountDialog = ({
<Text <Text
css={{ css={{
fontFamily: "$monospace", fontFamily: "$monospace",
a: { "&:hover": { textDecoration: "underline" } },
}} }}
>
<a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noopener noreferrer"
> >
{activeAccount?.address} {activeAccount?.address}
</a>
</Text> </Text>
</Flex> </Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}> <Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
@@ -222,11 +215,7 @@ export const AccountDialog = ({
</Button> </Button>
</Text> </Text>
</Flex> </Flex>
<Flex <Flex css={{ marginLeft: "auto" }}>
css={{
marginLeft: "auto",
}}
>
<a <a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`} href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank" target="_blank"
@@ -248,22 +237,10 @@ export const AccountDialog = ({
<Text <Text
css={{ css={{
fontFamily: "$monospace", fontFamily: "$monospace",
a: { "&:hover": { textDecoration: "underline" } },
}} }}
> >
{activeAccount && activeAccount.hooks.length > 0 {activeAccount && activeAccount.hooks.length > 0
? activeAccount.hooks.map((i) => { ? activeAccount.hooks.map((i) => truncate(i, 12)).join(",")
return (
<a
key={i}
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
target="_blank"
rel="noopener noreferrer"
>
{truncate(i, 12)}
</a>
);
})
: ""} : ""}
</Text> </Text>
</Flex> </Flex>
@@ -327,18 +304,6 @@ const Accounts: FC<AccountProps> = (props) => {
if (accountToUpdate) { if (accountToUpdate) {
accountToUpdate.xrp = balance; accountToUpdate.xrp = balance;
accountToUpdate.sequence = sequence; accountToUpdate.sequence = sequence;
accountToUpdate.error = null;
} else {
const oldAccount = state.accounts.find(
(acc) => acc.address === res?.account
);
if (oldAccount) {
oldAccount.xrp = "0";
oldAccount.error = {
code: res?.error,
message: res?.error_message,
};
}
} }
}); });
const objectRequests = snap.accounts.map((acc) => { const objectRequests = snap.accounts.map((acc) => {
@@ -378,7 +343,7 @@ const Accounts: FC<AccountProps> = (props) => {
} }
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [snap.accounts.length, snap.clientStatus]); }, [snap.accounts, snap.clientStatus]);
return ( return (
<Box <Box
as="div" as="div"
@@ -466,9 +431,8 @@ const Accounts: FC<AccountProps> = (props) => {
wordBreak: "break-word", wordBreak: "break-word",
}} }}
> >
{account.address}{" "} {account.address} (
{!account?.error ? ( {Dinero({
`(${Dinero({
amount: Number(account?.xrp || "0"), amount: Number(account?.xrp || "0"),
precision: 6, precision: 6,
}) })
@@ -477,12 +441,8 @@ const Accounts: FC<AccountProps> = (props) => {
style: "currency", style: "currency",
currency: "XRP", currency: "XRP",
currencyDisplay: "name", currencyDisplay: "name",
})})` })}
) : ( )
<Box css={{ color: "$red11" }}>
(Account not found, request funds to activate account)
</Box>
)}
</Text> </Text>
</Box> </Box>
{!props.hideDeployBtn && ( {!props.hideDeployBtn && (
@@ -492,7 +452,7 @@ const Accounts: FC<AccountProps> = (props) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<SetHookDialog accountAddress={account.address} /> <SetHookDialog account={account} />
</div> </div>
)} )}
</Flex> </Flex>
@@ -531,11 +491,10 @@ const ImportAccountDialog = () => {
<DialogContent> <DialogContent>
<DialogTitle>Import account</DialogTitle> <DialogTitle>Import account</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>Add account secret</Label> <label>Add account secret</label>
<Input <Input
name="secret" name="secret"
type="password" type="password"
autoComplete="new-password"
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
/> />

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { blackA } from "@radix-ui/colors"; import { blackA } from "@radix-ui/colors";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { styled, keyframes } from "../../stitches.config"; import { styled, keyframes } from "../stitches.config";
const overlayShow = keyframes({ const overlayShow = keyframes({
"0%": { opacity: 0 }, "0%": { opacity: 0 },
@@ -75,7 +75,7 @@ const StyledDescription = styled(AlertDialogPrimitive.Description, {
marginBottom: 20, marginBottom: 20,
color: "$mauve11", color: "$mauve11",
lineHeight: 1.5, lineHeight: 1.5,
fontSize: "$md", fontSize: "$sm",
}); });
// Exports // Exports

View File

@@ -1,75 +0,0 @@
import { FC, ReactNode } from "react";
import { proxy, useSnapshot } from "valtio";
import Button from "../Button";
import Flex from "../Flex";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from "./primitive";
export interface AlertState {
isOpen: boolean;
title?: string;
body?: ReactNode;
cancelText?: string;
confirmText?: string;
confirmPrefix?: ReactNode;
onConfirm?: () => any;
onCancel?: () => any;
}
export const alertState = proxy<AlertState>({
isOpen: false,
});
const Alert: FC = () => {
const {
title = "Are you sure?",
isOpen,
body,
cancelText,
confirmText = "Ok",
confirmPrefix,
onCancel,
onConfirm,
} = useSnapshot(alertState);
return (
<AlertDialog
open={isOpen}
onOpenChange={value => (alertState.isOpen = value)}
>
<AlertDialogContent>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{body}</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
{(cancelText || onCancel) && (
<AlertDialogCancel asChild>
<Button css={{ minWidth: "$16" }} outline onClick={onCancel}>
{cancelText || "Cancel"}
</Button>
</AlertDialogCancel>
)}
<AlertDialogAction asChild>
<Button
css={{ minWidth: "$16" }}
variant="primary"
onClick={async () => {
await onConfirm?.();
alertState.isOpen = false;
}}
>
{confirmPrefix}
{confirmText}
</Button>
</AlertDialogAction>
</Flex>
</AlertDialogContent>
</AlertDialog>
);
};
export default Alert;

View File

@@ -1,9 +1,7 @@
import { useEffect } from "react"; import { useCallback, useEffect } from "react";
import ReconnectingWebSocket, { CloseEvent } from "reconnecting-websocket";
import { proxy, ref, useSnapshot } from "valtio"; import { proxy, ref, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils";
import { Select } from "."; import { Select } from ".";
import state, { ILog, transactionsState } from "../state"; import state, { ILog } from "../state";
import { extractJSON } from "../utils/json"; import { extractJSON } from "../utils/json";
import LogBox from "./LogBox"; import LogBox from "./LogBox";
@@ -12,96 +10,14 @@ interface ISelect<T = string> {
value: T; value: T;
} }
export interface IStreamState { const streamState = proxy({
selectedAccount: ISelect | null;
status: "idle" | "opened" | "closed";
statusChangeTimestamp?: number;
logs: ILog[];
socket?: ReconnectingWebSocket;
}
export const streamState = proxy<IStreamState>({
selectedAccount: null as ISelect | null, selectedAccount: null as ISelect | null,
status: "idle",
logs: [] as ILog[], logs: [] as ILog[],
socket: undefined as WebSocket | undefined,
}); });
const onOpen = (account: ISelect | null) => {
if (!account) {
return;
}
// streamState.logs = [];
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account?.value}`, {
type: "success",
});
};
const onError = () => {
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
});
};
const onClose = (e: CloseEvent) => {
// 999 = closed websocket connection by switching account
if (e.code !== 4999) {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
});
}
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
// Ping returns just account address, if we get that
// response we don't need to log anything
if (event.data !== streamState.selectedAccount?.value) {
pushLog(event.data);
}
};
let interval: NodeJS.Timer | null = null;
const addListeners = (account: ISelect | null) => {
if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
return;
}
streamState.logs = [];
if (account?.value) {
if (interval) {
clearInterval(interval);
}
if (streamState.socket) {
streamState.socket?.removeEventListener("open", () => onOpen(account));
streamState.socket?.removeEventListener("close", onClose);
streamState.socket?.removeEventListener("error", onError);
streamState.socket?.removeEventListener("message", onMessage);
}
streamState.socket = ref(
new ReconnectingWebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
)
);
if (streamState.socket) {
interval = setInterval(() => {
streamState.socket?.send("");
}, 10000);
}
streamState.socket.addEventListener("open", () => onOpen(account));
streamState.socket.addEventListener("close", onClose);
streamState.socket.addEventListener("error", onError);
streamState.socket.addEventListener("message", onMessage);
}
};
subscribeKey(streamState, "selectedAccount", addListeners);
const DebugStream = () => { const DebugStream = () => {
const { selectedAccount, logs } = useSnapshot(streamState); const { selectedAccount, logs, socket } = useSnapshot(streamState);
const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
const { accounts } = useSnapshot(state); const { accounts } = useSnapshot(state);
const accountOptions = accounts.map((acc) => ({ const accountOptions = accounts.map((acc) => ({
@@ -117,59 +33,23 @@ const DebugStream = () => {
options={accountOptions} options={accountOptions}
hideSelectedOptions hideSelectedOptions
value={selectedAccount} value={selectedAccount}
onChange={(acc) => { onChange={(acc) => (streamState.selectedAccount = acc as any)}
streamState.socket?.close(
4999,
"Old connection closed because user switched account"
);
streamState.selectedAccount = acc as any;
}}
css={{ width: "100%" }} css={{ width: "100%" }}
/> />
</> </>
); );
useEffect(() => { const prepareLog = useCallback((str: any): ILog => {
const account = transactionsState.transactions.find(
(tx) => tx.header === activeTxTab
)?.state.selectedAccount;
if (account && account.value !== streamState.selectedAccount?.value)
streamState.selectedAccount = account;
}, [activeTxTab]);
const clearLog = () => {
streamState.logs = [];
streamState.statusChangeTimestamp = Date.now();
};
return (
<LogBox
enhanced
renderNav={renderNav}
title="Debug stream"
logs={logs}
clearLog={clearLog}
/>
);
};
export default DebugStream;
export const pushLog = (
str: any,
opts: Partial<Pick<ILog, "type">> = {}
): ILog | undefined => {
if (!str) return;
if (typeof str !== "string") throw Error("Unrecognized debug log stream!"); if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m); const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))\ ?([\s\S]*)/m);
const [_, tm, msg] = match || []; const [_, tm, msg] = match || [];
const timestamp = Date.parse(tm || "") || undefined;
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString();
const extracted = extractJSON(msg); const extracted = extractJSON(msg);
const timestamp = isNaN(Date.parse(tm || ""))
? tm
: new Date(tm).toLocaleTimeString();
const message = !extracted const message = !extracted
? msg ? msg
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1); : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
@@ -177,20 +57,91 @@ export const pushLog = (
const jsonData = extracted const jsonData = extracted
? JSON.stringify(extracted.result, null, 2) ? JSON.stringify(extracted.result, null, 2)
: undefined; : undefined;
return {
if (extracted?.result?.id?._Request?.includes("hooks-builder-req")) { type: "log",
return;
}
const { type = "log" } = opts;
const log: ILog = {
type,
message, message,
timestring, timestamp,
jsonData, jsonData,
defaultCollapsed: true, defaultCollapsed: true,
}; };
}, []);
if (log) streamState.logs.push(log); useEffect(() => {
return log; const account = selectedAccount?.value;
if (account && (!socket || !socket.url.endsWith(account))) {
socket?.close();
streamState.socket = ref(
new WebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account}`
)
);
} else if (!account && socket) {
socket.close();
streamState.socket = undefined;
}
}, [selectedAccount?.value, socket]);
useEffect(() => {
const account = selectedAccount?.value;
const socket = streamState.socket;
if (!socket) return;
const onOpen = () => {
streamState.logs = [];
streamState.logs.push({
type: "success",
message: `Debug stream opened for account ${account}`,
});
};
const onError = () => {
streamState.logs.push({
type: "error",
message: "Something went wrong! Check your connection and try again.",
});
};
const onClose = (e: CloseEvent) => {
streamState.logs.push({
type: "error",
message: `Connection was closed. [code: ${e.code}]`,
});
streamState.selectedAccount = null;
};
const onMessage = (event: any) => {
if (!event.data) return;
const log = prepareLog(event.data);
// Filter out account_info and account_objects requests
try {
const parsed = JSON.parse(log.jsonData);
if (parsed?.id?._Request?.includes("hooks-builder-req")) {
return;
}
} catch (err) {
// Lets just skip if we cannot parse the message
}
return streamState.logs.push(log);
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onClose);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
};
}, [prepareLog, selectedAccount?.value, socket]);
return (
<LogBox
enhanced
renderNav={renderNav}
title="Debug stream"
logs={logs}
clearLog={() => (streamState.logs = [])}
/>
);
}; };
export default DebugStream;

View File

@@ -34,9 +34,7 @@ const DeployEditor = () => {
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
const activeFile = snap.files[snap.active]?.compiledContent const activeFile = snap.files[snap.active];
? snap.files[snap.active]
: snap.files.filter((file) => file.compiledContent)[0];
const compiledSize = activeFile?.compiledContent?.byteLength || 0; const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color = const color =
compiledSize > FILESIZE_BREAKPOINTS[1] compiledSize > FILESIZE_BREAKPOINTS[1]
@@ -62,21 +60,12 @@ const DeployEditor = () => {
{activeFile?.lastCompiled && ( {activeFile?.lastCompiled && (
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" /> <ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
)} )}
{activeFile.compiledContent?.byteLength && ( {activeFile.compiledContent?.byteLength && (
<Text css={{ ml: "$2", color }}> <Text css={{ ml: "$2", color }}>
({filesize(activeFile.compiledContent.byteLength)}) ({filesize(activeFile.compiledContent.byteLength)})
</Text> </Text>
)} )}
</Flex> </Flex>
{activeFile.compiledContent?.byteLength &&
activeFile.compiledContent?.byteLength >= 64000 && (
<Flex css={{ flexDirection: "column", py: "$3", pb: "$1" }}>
<Text css={{ ml: "$2", color: "$error" }}>
File size is larger than 64kB, cannot set hook!
</Text>
</Flex>
)}
<Button variant="link" onClick={() => setShowContent(true)}> <Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file View as WAT-file
</Button> </Button>
@@ -130,8 +119,8 @@ const DeployEditor = () => {
className="hooks-editor" className="hooks-editor"
defaultLanguage={"wat"} defaultLanguage={"wat"}
language={"wat"} language={"wat"}
path={`file://tmp/c/${activeFile?.name}.wat`} path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
value={activeFile?.compiledWatContent || ""} value={snap.files?.[snap.active]?.compiledWatContent || ""}
beforeMount={(monaco) => { beforeMount={(monaco) => {
monaco.languages.register({ id: "wat" }); monaco.languages.register({ id: "wat" });
monaco.languages.setLanguageConfiguration("wat", wat.config); monaco.languages.setLanguageConfiguration("wat", wat.config);

103
components/DeployFooter.tsx Normal file
View File

@@ -0,0 +1,103 @@
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 } 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"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
}}
>
<Container css={{ py: "$3", flexShrink: 1 }}>
<Heading
as="h3"
css={{ fontWeight: 300, m: 0, fontSize: "11px", color: "$mauve9" }}
>
DEVELOPMENT LOG
</Heading>
<Button
ghost
size="xs"
css={{
position: "absolute",
right: "$3",
top: "$2",
color: "$mauve10",
}}
onClick={() => {
state.logs = [];
}}
>
<Prohibit size="14px" />
</Button>
<Box
as="pre"
ref={logRef}
css={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
overflowY: "auto",
wordWrap: "break-word",
py: 3,
}}
>
{snap.logs?.map((log, index) => (
<Box as="span" key={log.type + index}>
<LogText capitalize variant={log.type}>
{log.type}:{" "}
</LogText>
<LogText>{log.message}</LogText>
</Box>
))}
</Box>
<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>
</Container>
</Box>
);
};
export default Footer;

View File

@@ -40,7 +40,6 @@ const StyledContent = styled(DialogPrimitive.Content, {
color: "$mauve12", color: "$mauve12",
borderRadius: "$md", borderRadius: "$md",
position: "relative", position: "relative",
mb: "15%",
boxShadow: boxShadow:
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)", "0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
width: "90vw", width: "90vw",

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { import {
Plus, Plus,
Share, Share,
@@ -47,11 +47,18 @@ import {
} from "./Dialog"; } from "./Dialog";
import Flex from "./Flex"; import Flex from "./Flex";
import Stack from "./Stack"; import Stack from "./Stack";
import { Input, Label } from "./Input"; import Input from "./Input";
import Text from "./Text"; import Text from "./Text";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import {
AlertDialog,
AlertDialogContent,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogCancel,
AlertDialogAction,
} from "./AlertDialog";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import { showAlert } from "../state/actions/showAlert";
const ErrorText = styled(Text, { const ErrorText = styled(Text, {
color: "$error", color: "$error",
@@ -61,6 +68,7 @@ const ErrorText = styled(Text, {
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => { const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false); const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false); const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null); const [newfileError, setNewfileError] = useState<string | null>(null);
@@ -79,22 +87,6 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
setNewfileError(null); setNewfileError(null);
}, [filename, setNewfileError]); }, [filename, setNewfileError]);
const showNewGistAlert = () => {
showAlert("Are you sure?", {
body: (
<>
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.
</>
),
cancelText: "Cancel",
confirmText: "Create new Gist",
confirmPrefix: <FilePlus size="15px" />,
onConfirm: () => syncToGist(session, true),
});
};
const validateFilename = useCallback( const validateFilename = useCallback(
(filename: string): { error: string | null } => { (filename: string): { error: string | null } => {
// check if filename already exists // check if filename already exists
@@ -132,55 +124,22 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
createNewFile(filename); createNewFile(filename);
setFilename(""); setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]); }, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const scrollRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const files = snap.files; const files = snap.files;
return ( return (
<Flex css={{ flexShrink: 0, gap: "$0" }}> <Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex <Flex
id="kissa"
ref={scrollRef}
css={{ css={{
overflowX: "scroll", overflowX: "scroll",
overflowY: "hidden",
py: "$3", py: "$3",
pb: "$0",
flex: 1, flex: 1,
"&::-webkit-scrollbar": { "&::-webkit-scrollbar": {
height: "0.3em", height: 0,
background: "rgba(0,0,0,.0)", background: "transparent",
}, },
"&::-webkit-scrollbar-gutter": "stable",
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(0,0,0,.2)",
outline: "0px",
borderRadius: "9999px",
},
scrollbarColor: "rgba(0,0,0,.2) rgba(0,0,0,0)",
scrollbarGutter: "stable",
scrollbarWidth: "thin",
".dark &": {
"&::-webkit-scrollbar": {
background: "rgba(0,0,0,.0)",
},
"&::-webkit-scrollbar-gutter": "stable",
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(255,255,255,.2)",
outline: "0px",
borderRadius: "9999px",
},
scrollbarColor: "rgba(255,255,255,.2) rgba(0,0,0,0)",
scrollbarGutter: "stable",
scrollbarWidth: "thin",
},
}}
onWheelCapture={(e) => {
if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY;
}
}} }}
> >
<Container css={{ flex: 1 }} ref={containerRef}> <Container css={{ flex: 1 }}>
<Stack <Stack
css={{ css={{
gap: "$3", gap: "$3",
@@ -263,7 +222,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<DialogContent> <DialogContent>
<DialogTitle>Create new file</DialogTitle> <DialogTitle>Create new file</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>Filename</Label> <label>Filename</label>
<Input <Input
value={filename} value={filename}
onChange={(e) => setFilename(e.target.value)} onChange={(e) => setFilename(e.target.value)}
@@ -457,7 +416,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
if (snap.gistOwner === session?.user.username) { if (snap.gistOwner === session?.user.username) {
syncToGist(session); syncToGist(session);
} else { } else {
showNewGistAlert(); setCreateNewAlertOpen(true);
} }
}} }}
> >
@@ -507,7 +466,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<DropdownMenuItem <DropdownMenuItem
disabled={status !== "authenticated"} disabled={status !== "authenticated"}
onClick={() => { onClick={() => {
showNewGistAlert(); setCreateNewAlertOpen(true);
}} }}
> >
<FilePlus size="16px" /> Create as a new Gist <FilePlus size="16px" /> Create as a new Gist
@@ -527,6 +486,34 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
) : null} ) : null}
</Container> </Container>
</Flex> </Flex>
<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.
</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
<AlertDialogCancel asChild>
<Button outline>Cancel</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
variant="primary"
onClick={() => {
syncToGist(session, true);
}}
>
<FilePlus size="15px" /> Create new Gist
</Button>
</AlertDialogAction>
</Flex>
</AlertDialogContent>
</AlertDialog>
<Dialog open={editorSettingsOpen} onOpenChange={setEditorSettingsOpen}> <Dialog open={editorSettingsOpen} onOpenChange={setEditorSettingsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -537,7 +524,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<DialogContent> <DialogContent>
<DialogTitle>Editor settings</DialogTitle> <DialogTitle>Editor settings</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>Tab size</Label> <label>Tab size</label>
<Input <Input
type="number" type="number"
min="1" min="1"

View File

@@ -1,10 +1,11 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio"; import { useSnapshot, ref } from "valtio";
import Editor from "@monaco-editor/react"; import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
import { ArrowBendLeftUp } from "phosphor-react"; import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import uniqBy from "lodash.uniqby";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
@@ -23,6 +24,12 @@ import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs"; import docs from "../xrpl-hooks-docs/docs";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path; const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) { if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
@@ -38,7 +45,8 @@ const setMarkers = (monacoE: typeof monaco) => {
// Get all the markers that are active at the moment, // Get all the markers that are active at the moment,
// Also if same error is there twice, we can show the content // Also if same error is there twice, we can show the content
// only once (that's why we're using uniqBy) // only once (that's why we're using uniqBy)
const markers = monacoE.editor const markers = uniqBy(
monacoE.editor
.getModelMarkers({}) .getModelMarkers({})
// Filter out the markers that are hooks specific // Filter out the markers that are hooks specific
.filter( .filter(
@@ -46,6 +54,8 @@ const setMarkers = (monacoE: typeof monaco) => {
typeof marker?.code === "string" && typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-" // Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-") marker?.code?.includes("hooks-")
),
"code"
); );
// Get the active model (aka active file you're editing) // Get the active model (aka active file you're editing)
@@ -165,9 +175,9 @@ const HooksEditor = () => {
// create and start the language client // create and start the language client
const languageClient = createLanguageClient(connection); const languageClient = createLanguageClient(connection);
const disposable = languageClient.start(); const disposable = languageClient.start();
connection.onClose(() => { connection.onClose(() => {
try { try {
// disposable.stop();
disposable.dispose(); disposable.dispose();
} catch (err) { } catch (err) {
console.log("err", err); console.log("err", err);

View File

@@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import * as LabelPrim from '@radix-ui/react-label';
export const Input = styled("input", { export const Input = styled("input", {
// Reset // Reset
@@ -159,11 +158,3 @@ const ReffedInput = React.forwardRef<
>((props, ref) => <Input {...props} ref={ref} />); >((props, ref) => <Input {...props} ref={ref} />);
export default ReffedInput; export default ReffedInput;
const LabelRoot = (props: LabelPrim.LabelProps) => <LabelPrim.Root {...props} />
export const Label = styled(LabelRoot, {
display: 'inline-block',
mb: '$1'
})

View File

@@ -6,7 +6,7 @@ import {
useState, useState,
useCallback, useCallback,
} from "react"; } from "react";
import { IconProps, Notepad, Prohibit } from "phosphor-react"; import { Notepad, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled"; import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link"; import NextLink from "next/link";
@@ -24,7 +24,6 @@ interface ILogBox {
logs: ILog[]; logs: ILog[];
renderNav?: () => ReactNode; renderNav?: () => ReactNode;
enhanced?: boolean; enhanced?: boolean;
Icon?: FC<IconProps>;
} }
const LogBox: FC<ILogBox> = ({ const LogBox: FC<ILogBox> = ({
@@ -34,7 +33,6 @@ const LogBox: FC<ILogBox> = ({
children, children,
renderNav, renderNav,
enhanced, enhanced,
Icon = Notepad,
}) => { }) => {
const logRef = useRef<HTMLPreElement>(null); const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef); const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
@@ -84,14 +82,14 @@ const LogBox: FC<ILogBox> = ({
gap: "$3", gap: "$3",
}} }}
> >
<Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text> <Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading> </Heading>
<Flex <Flex
row row
align="center" align="center"
// css={{ css={{
// maxWidth: "100%", // TODO make it max without breaking layout! width: "50%", // TODO make it max without breaking layout!
// }} }}
> >
{renderNav?.()} {renderNav?.()}
</Flex> </Flex>
@@ -149,7 +147,7 @@ const LogBox: FC<ILogBox> = ({
export const Log: FC<ILog> = ({ export const Log: FC<ILog> = ({
type, type,
timestring, timestamp: timestamp,
message: _message, message: _message,
link, link,
linkText, linkText,
@@ -164,11 +162,11 @@ export const Log: FC<ILog> = ({
(str?: string): ReactNode => { (str?: string): ReactNode => {
if (!str || !accounts.length) return null; if (!str || !accounts.length) return null;
const pattern = `(${accounts.map(acc => acc.address).join("|")})`; const pattern = `(${accounts.map((acc) => acc.address).join("|")})`;
const res = regexifyString({ const res = regexifyString({
pattern: new RegExp(pattern, "gim"), pattern: new RegExp(pattern, "gim"),
decorator: (match, idx) => { decorator: (match, idx) => {
const name = accounts.find(acc => acc.address === match)?.name; const name = accounts.find((acc) => acc.address === match)?.name;
return ( return (
<Link <Link
key={match + idx} key={match + idx}
@@ -188,17 +186,8 @@ export const Log: FC<ILog> = ({
}, },
[accounts] [accounts]
); );
let message: ReactNode;
if (typeof _message === "string") {
_message = _message.trim().replace(/\n /gi, "\n"); _message = _message.trim().replace(/\n /gi, "\n");
if (_message) message = enrichAccounts(_message); const message = enrichAccounts(_message);
else message = <Text muted>{'""'}</Text>
} else {
message = _message;
}
const jsonData = enrichAccounts(_jsonData); const jsonData = enrichAccounts(_jsonData);
return ( return (
@@ -208,9 +197,9 @@ export const Log: FC<ILog> = ({
activeAccountAddress={dialogAccount} activeAccountAddress={dialogAccount}
/> />
<LogText variant={type}> <LogText variant={type}>
{timestring && ( {timestamp && (
<Text muted monospace> <Text muted monospace>
{timestring}{" "} {timestamp}{" "}
</Text> </Text>
)} )}
<Pre>{message} </Pre> <Pre>{message} </Pre>

View File

@@ -28,22 +28,6 @@ import {
} from "./Dialog"; } from "./Dialog";
import PanelBox from "./PanelBox"; import PanelBox from "./PanelBox";
import { templateFileIds } from "../state/constants"; import { templateFileIds } from "../state/constants";
import { styled } from "../stitches.config";
const ImageWrapper = styled(Flex, {
position: "relative",
mt: "$2",
mb: "$10",
svg: {
// fill: "red",
".angle": {
fill: "$text",
},
":not(.angle)": {
stroke: "$text",
},
},
});
const Navigation = () => { const Navigation = () => {
const router = useRouter(); const router = useRouter();
@@ -107,7 +91,7 @@ const Navigation = () => {
<Text <Text
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }} css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
> >
{snap.files.length > 0 ? "Gist: " : "Builder"} {snap.files.length > 0 ? "Gist: " : "Playground"}
{snap.files.length > 0 && ( {snap.files.length > 0 && (
<Link <Link
href={`https://gist.github.com/${snap.gistOwner || ""}/${ href={`https://gist.github.com/${snap.gistOwner || ""}/${
@@ -144,20 +128,19 @@ const Navigation = () => {
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
css={{ css={{
display: "flex",
maxWidth: "1080px", maxWidth: "1080px",
width: "80vw", width: "80vw",
maxHeight: "80%", height: "80%",
backgroundColor: "$mauve1 !important", backgroundColor: "$mauve1 !important",
overflowY: "auto", overflowY: "auto",
background: "black",
p: 0, p: 0,
}} }}
> >
<Flex <Flex
css={{ css={{
flexDirection: "column", flexDirection: "column",
height: "100%", flex: 1,
height: "auto",
"@md": { "@md": {
flexDirection: "row", flexDirection: "row",
height: "100%", height: "100%",
@@ -168,15 +151,15 @@ const Navigation = () => {
css={{ css={{
borderBottom: "1px solid $colors$mauve5", borderBottom: "1px solid $colors$mauve5",
width: "100%", width: "100%",
minWidth: "240px",
flexDirection: "column", flexDirection: "column",
p: "$7", p: "$7",
height: "100%",
backgroundColor: "$mauve2", backgroundColor: "$mauve2",
"@md": { "@md": {
width: "30%", width: "30%",
maxWidth: "300px", maxWidth: "300px",
borderBottom: "0px", borderBottom: "0px",
borderRight: "1px solid $colors$mauve5", borderRight: "1px solid $colors$mauve6",
}, },
}} }}
> >
@@ -213,9 +196,9 @@ const Navigation = () => {
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
gap: "$3", gap: "$3",
color: "$purple11", color: "$purple10",
"&:hover": { "&:hover": {
color: "$purple12", color: "$purple11",
}, },
"&:focus": { "&:focus": {
outline: 0, outline: 0,
@@ -234,9 +217,9 @@ const Navigation = () => {
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
gap: "$3", gap: "$3",
color: "$purple11", color: "$purple10",
"&:hover": { "&:hover": {
color: "$purple12", color: "$purple11",
}, },
"&:focus": { "&:focus": {
outline: 0, outline: 0,
@@ -254,9 +237,9 @@ const Navigation = () => {
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
gap: "$3", gap: "$3",
color: "$purple11", color: "$purple10",
"&:hover": { "&:hover": {
color: "$purple12", color: "$purple11",
}, },
"&:focus": { "&:focus": {
outline: 0, outline: 0,
@@ -272,7 +255,7 @@ const Navigation = () => {
</Flex> </Flex>
</DialogDescription> </DialogDescription>
</Flex> </Flex>
<div>
<Flex <Flex
css={{ css={{
display: "grid", display: "grid",
@@ -280,34 +263,59 @@ const Navigation = () => {
gridTemplateRows: "max-content", gridTemplateRows: "max-content",
flex: 1, flex: 1,
p: "$7", p: "$7",
pb: "$16",
gap: "$3", gap: "$3",
alignItems: "normal", alignItems: "normal",
flexWrap: "wrap", flexWrap: "wrap",
backgroundColor: "$mauve1", backgroundColor: "$mauve1",
"@md": { "@md": {
gridTemplateColumns: "1fr 1fr",
gridTemplateRows: "max-content",
},
"@lg": {
gridTemplateColumns: "1fr 1fr 1fr", gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "max-content", gridTemplateRows: "max-content",
}, },
}} }}
> >
{Object.values(templateFileIds).map((template) => (
<PanelBox <PanelBox
key={template.id}
as="a" as="a"
href={`/develop/${template.id}`} href={`/develop/${templateFileIds.starter}`}
> >
<ImageWrapper>{template.icon()}</ImageWrapper> <Heading>Starter</Heading>
<Heading>{template.name}</Heading> <Text>
Just a basic starter with essential imports
<Text>{template.description}</Text> </Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.firewall}`}
>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</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 stable coin hook</Text>
</PanelBox> </PanelBox>
))}
</Flex> </Flex>
</div>
</Flex> </Flex>
<DialogClose asChild> <DialogClose asChild>
<Box <Box
@@ -340,8 +348,6 @@ const Navigation = () => {
height: 0, height: 0,
background: "transparent", background: "transparent",
}, },
scrollbarColor: "transparent",
scrollbarWidth: "none",
}} }}
> >
<Stack <Stack

View File

@@ -1,109 +0,0 @@
import React, { ReactNode } from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { styled, keyframes } from "../stitches.config";
const slideUpAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideRightAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(-2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const slideDownAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(-2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideLeftAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const StyledContent = styled(PopoverPrimitive.Content, {
borderRadius: 4,
padding: "$3 $3",
fontSize: 12,
lineHeight: 1,
color: "$text",
backgroundColor: "$background",
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
"@media (prefers-reduced-motion: no-preference)": {
animationDuration: "400ms",
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
willChange: "transform, opacity",
'&[data-state="open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
"0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)",
},
});
const StyledArrow = styled(PopoverPrimitive.Arrow, {
fill: "$colors$mauve2",
".dark &": {
fill: "$mauve5",
},
});
const StyledClose = styled(PopoverPrimitive.Close, {
all: "unset",
fontFamily: "inherit",
borderRadius: "100%",
height: 25,
width: 25,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
color: "$text",
position: "absolute",
top: 5,
right: 5,
});
// Exports
export const PopoverRoot = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverContent = StyledContent;
export const PopoverArrow = StyledArrow;
export const PopoverClose = StyledClose;
interface IPopover {
content: string | ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
}
const Popover: React.FC<
IPopover & React.ComponentProps<typeof PopoverContent>
> = ({
children,
content,
open,
defaultOpen = false,
onOpenChange,
...rest
}) => (
<PopoverRoot
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent sideOffset={5} {...rest}>
{content} <PopoverArrow offset={5} className="arrow" />
</PopoverContent>
</PopoverRoot>
);
export default Popover;

View File

@@ -1,338 +0,0 @@
import { Play, X } from "phosphor-react";
import {
HTMLInputTypeAttribute,
useCallback,
useEffect,
useState,
} from "react";
import state, { IAccount, IFile, ILog } from "../../state";
import Button from "../Button";
import Box from "../Box";
import Input, { Label } from "../Input";
import Stack from "../Stack";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
} from "../Dialog";
import Flex from "../Flex";
import { useSnapshot } from "valtio";
import Select from "../Select";
import Text from "../Text";
import { saveFile } from "../../state/actions/saveFile";
import { getErrors, getTags } from "../../utils/comment-parser";
import toast from "react-hot-toast";
const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
let processString: string | undefined;
const process = { env: { NODE_ENV: "production" } } as any;
if (data) {
Object.keys(data).forEach(key => {
process.env[key] = data[key];
});
}
processString = JSON.stringify(process);
return `
<html>
<head>
<script>
var log = console.log;
var errorLog = console.error;
var infoLog = console.info;
var warnLog = console.warn
console.log = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'log', args: args || [] }, '*');
log.apply(console, args);
}
console.error = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'error', args: args || [] }, '*');
errorLog.apply(console, args);
}
console.info = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'info', args: args || [] }, '*');
infoLog.apply(console, args);
}
console.warn = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'warning', args: args || [] }, '*');
warnLog.apply(console, args);
}
var process = '${processString || "{}"}';
process = JSON.parse(process);
window.process = process
function windowErrorHandler(event) {
event.preventDefault() // to prevent automatically logging to console
console.error(event.error?.toString())
}
window.addEventListener('error', windowErrorHandler);
</script>
<script type="module">
${code}
</script>
</head>
<body>
</body>
</html>
`;
};
type Fields = Record<
string,
{
name: string;
value: string;
type?: "Account" | `Account.${keyof IAccount}` | HTMLInputTypeAttribute;
description?: string;
required?: boolean;
}
>;
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
const snap = useSnapshot(state);
const [templateError, setTemplateError] = useState("");
const [fields, setFields] = useState<Fields>({});
const [iFrameCode, setIframeCode] = useState("");
const [isDialogOpen, setIsDialogOpen] = useState(false);
const getFields = useCallback(() => {
const inputTags = ["input", "param", "arg", "argument"];
const tags = getTags(content)
.filter(tag => inputTags.includes(tag.tag))
.filter(tag => !!tag.name);
let _fields = tags.map(tag => ({
name: tag.name,
value: tag.default || "",
type: tag.type,
description: tag.description,
required: !tag.optional,
}));
const fields: Fields = _fields.reduce((acc, field) => {
acc[field.name] = field;
return acc;
}, {} as Fields);
const error = getErrors(content);
if (error) setTemplateError(error.message);
else setTemplateError("");
return fields;
}, [content]);
const runScript = useCallback(() => {
try {
let data: any = {};
Object.keys(fields).forEach(key => {
data[key] = fields[key].value;
});
const template = generateHtmlTemplate(content, data);
setIframeCode(template);
state.scriptLogs = [
...snap.scriptLogs,
{ type: "success", message: "Started running..." },
];
} catch (err) {
state.scriptLogs = [
...snap.scriptLogs,
// @ts-expect-error
{ type: "error", message: err?.message || "Could not parse template" },
];
}
}, [content, fields, snap.scriptLogs]);
useEffect(() => {
const handleEvent = (e: any) => {
if (e.data.type === "log" || e.data.type === "error") {
const data: ILog[] = e.data.args.map((msg: any) => ({
type: e.data.type,
message: typeof msg === "string" ? msg : JSON.stringify(msg, null, 2),
}));
state.scriptLogs = [...snap.scriptLogs, ...data];
}
};
window.addEventListener("message", handleEvent);
return () => window.removeEventListener("message", handleEvent);
}, [snap.scriptLogs]);
useEffect(() => {
const defaultFields = getFields() || {};
setFields(defaultFields);
}, [content, setFields, getFields]);
const accOptions = snap.accounts?.map(acc => ({
...acc,
label: acc.name,
value: acc.address,
}));
const isDisabled = Object.values(fields).some(
field => field.required && !field.value
);
const handleRun = useCallback(() => {
if (isDisabled)
return toast.error("Please fill in all the required fields.");
state.scriptLogs = [];
runScript();
setIsDialogOpen(false);
}, [isDisabled, runScript]);
return (
<>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
variant="primary"
onClick={() => {
saveFile(false);
setIframeCode("");
}}
>
<Play weight="bold" size="16px" /> {name}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Run {name} script</DialogTitle>
<DialogDescription>
<Box>
You are about to run scripts provided by the developer of the
hook, make sure you trust the author before you continue.
</Box>
{templateError && (
<Box
as="span"
css={{
display: "block",
color: "$error",
mt: "$3",
whiteSpace: "pre",
}}
>
{templateError}
</Box>
)}
{Object.keys(fields).length > 0 && (
<Box css={{ mt: "$4", mb: 0 }}>
Fill in the following parameters to run the script.
</Box>
)}
</DialogDescription>
<Stack css={{ width: "100%" }}>
{Object.keys(fields).map(key => {
const { name, value, type, description, required } = fields[key];
const isAccount = type?.startsWith("Account");
const isAccountSecret = type === "Account.secret";
const accountField =
(isAccount && type?.split(".")[1]) || "address";
return (
<Box key={name} css={{ width: "100%" }}>
<Label
css={{ display: "flex", justifyContent: "space-between" }}
>
<span>
{description || name} {required && <Text error>*</Text>}
</span>
{isAccountSecret && (
<Text error small css={{ alignSelf: "end" }}>
can access account secret key
</Text>
)}
</Label>
{isAccount ? (
<Select
css={{ mt: "$1" }}
options={accOptions}
onChange={(val: any) => {
setFields({
...fields,
[key]: {
...fields[key],
value: val[accountField],
},
});
}}
value={accOptions.find(
(acc: any) => acc[accountField] === value
)}
/>
) : (
<Input
type={type || "text"}
value={value}
css={{ mt: "$1" }}
onChange={e => {
setFields({
...fields,
[key]: { ...fields[key], value: e.target.value },
});
}}
/>
)}
</Box>
);
})}
<Flex
css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button
variant="primary"
isDisabled={isDisabled}
onClick={handleRun}
>
Run script
</Button>
</Flex>
</Stack>
<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>
{iFrameCode && (
<iframe
style={{ display: "none" }}
srcDoc={iFrameCode}
sandbox="allow-scripts"
/>
)}
</>
);
};
export default RunScript;

View File

@@ -52,7 +52,6 @@ const Select = forwardRef<any, Props>((props, ref) => {
control: (provided, state) => { control: (provided, state) => {
return { return {
...provided, ...provided,
minHeight: 0,
border: "0px", border: "0px",
backgroundColor: colors.mauve4, backgroundColor: colors.mauve4,
boxShadow: `0 0 0 1px ${ boxShadow: `0 0 0 1px ${
@@ -119,6 +118,32 @@ const Select = forwardRef<any, Props>((props, ref) => {
}; };
}, },
}} }}
// theme={(theme) => ({
// ...theme,
// spacing: {
// ...theme.spacing,
// controlHeight: 30,
// },
// colors: {
// primary: colors.selected,
// primary25: colors.active,
// 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} {...props}
/> />
); );

View File

@@ -11,7 +11,7 @@ import {
DialogClose, DialogClose,
DialogTrigger, DialogTrigger,
} from "./Dialog"; } from "./Dialog";
import { Input, Label } from "./Input"; import { Input } from "./Input";
import { import {
Controller, Controller,
SubmitHandler, SubmitHandler,
@@ -21,13 +21,13 @@ import {
import { TTS, tts } from "../utils/hookOnCalculator"; import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions"; import { deployHook } from "../state/actions";
import type { IAccount } from "../state";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state, { SelectOption } from "../state"; import state from "../state";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook"; import { sha256 } from "../state/actions/deployHook";
import estimateFee from "../utils/estimateFee";
const transactionOptions = Object.keys(tts).map(key => ({ const transactionOptions = Object.keys(tts).map((key) => ({
label: key, label: key,
value: key as keyof TTS, value: key as keyof TTS,
})); }));
@@ -37,7 +37,6 @@ export type SetHookData = {
value: keyof TTS; value: keyof TTS;
label: string; label: string;
}[]; }[];
Fee: string;
HookNamespace: string; HookNamespace: string;
HookParameters: { HookParameters: {
HookParameter: { HookParameter: {
@@ -53,72 +52,24 @@ export type SetHookData = {
// }[]; // }[];
}; };
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo( export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
({ accountAddress }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const activeFile = snap.files[snap.active]?.compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false); const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState(
accountOptions.find(acc => acc.value === accountAddress)
);
const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value
);
const { const {
register, register,
handleSubmit, handleSubmit,
control, control,
watch, watch,
setValue,
getValues,
formState: { errors }, formState: { errors },
} = useForm<SetHookData>({ } = useForm<SetHookData>({
defaultValues: snap.deployValues?.[activeFile?.name] defaultValues: {
? snap.deployValues[activeFile?.name] HookNamespace: snap.files?.[snap.active]?.name?.split(".")?.[0] || "",
: {
HookNamespace:
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
}, },
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
name: "HookParameters", // unique name for your Field Array name: "HookParameters", // unique name for your Field Array
}); });
const [formInitialized, setFormInitialized] = useState(false);
const [estimateLoading, setEstimateLoading] = useState(false);
const watchedFee = watch("Fee");
// Update value if activeWat changes
useEffect(() => {
const defaultValue = snap.deployValues?.[activeFile?.name]
? snap.deployValues?.[activeFile?.name].HookNamespace
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "";
setValue("HookNamespace", defaultValue);
setFormInitialized(true);
}, [
snap.activeWat,
snap.files,
setValue,
activeFile?.name,
snap.deployValues,
]);
useEffect(() => {
if (
watchedFee &&
(watchedFee.includes(".") || watchedFee.includes(","))
) {
setValue("Fee", watchedFee.replaceAll(".", "").replaceAll(",", ""));
}
}, [watchedFee, setValue]);
// const { // const {
// fields: grantFields, // fields: grantFields,
// append: grantAppend, // append: grantAppend,
@@ -130,9 +81,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const [hashedNamespace, setHashedNamespace] = useState(""); const [hashedNamespace, setHashedNamespace] = useState("");
const namespace = watch( const namespace = watch(
"HookNamespace", "HookNamespace",
snap.deployValues?.[activeFile?.name] snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
? snap.deployValues?.[activeFile?.name].HookNamespace
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
); );
const calculateHashedValue = useCallback(async () => { const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace); const hashedVal = await sha256(namespace);
@@ -142,39 +91,14 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
calculateHashedValue(); calculateHashedValue();
}, [namespace, calculateHashedValue]); }, [namespace, calculateHashedValue]);
// Calcucate initial fee estimate when modal opens if (!account) {
useEffect(() => { return null;
if (formInitialized && account) {
(async () => {
const formValues = getValues();
const tx = await prepareDeployHookTx(account, formValues);
if (!tx) {
return;
} }
const res = await estimateFee(tx, account);
if (res && res.base_fee) {
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formInitialized]);
const tooLargeFile = () => {
const activeFile = snap.files[snap.active].compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
return Boolean(
activeFile?.compiledContent?.byteLength &&
activeFile?.compiledContent?.byteLength >= 64000
);
};
const onSubmit: SubmitHandler<SetHookData> = async (data) => { const onSubmit: SubmitHandler<SetHookData> = async (data) => {
const currAccount = state.accounts.find( const currAccount = state.accounts.find(
(acc) => acc.address === account?.address (acc) => acc.address === account.address
); );
if (!account) return;
if (currAccount) currAccount.isLoading = true; if (currAccount) currAccount.isLoading = true;
const res = await deployHook(account, data); const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false; if (currAccount) currAccount.isLoading = false;
@@ -185,6 +109,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
} }
toast.error(`Transaction failed! (${res?.engine_result_message})`); toast.error(`Transaction failed! (${res?.engine_result_message})`);
}; };
return ( return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}> <Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -194,10 +119,8 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
uppercase uppercase
variant={"secondary"} variant={"secondary"}
disabled={ disabled={
!account ||
account.isLoading || account.isLoading ||
!snap.files.filter(file => file.compiledWatContent).length || !snap.files.filter((file) => file.compiledWatContent).length
tooLargeFile()
} }
> >
Set Hook Set Hook
@@ -209,21 +132,13 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<DialogDescription as="div"> <DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}> <Stack css={{ width: "100%", flex: 1 }}>
<Box css={{ width: "100%" }}> <Box css={{ width: "100%" }}>
<Label>Account</Label> <label>Invoke on transactions</label>
<Select
instanceId="deploy-account"
placeholder="Select account"
hideSelectedOptions
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)}
/>
</Box>
<Box css={{ width: "100%" }}>
<Label>Invoke on transactions</Label>
<Controller <Controller
name="Invoke" name="Invoke"
control={control} control={control}
defaultValue={transactionOptions.filter(
(to) => to.label === "ttPAYMENT"
)}
render={({ field }) => ( render={({ field }) => (
<Select <Select
{...field} {...field}
@@ -236,10 +151,13 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
/> />
</Box> </Box>
<Box css={{ width: "100%" }}> <Box css={{ width: "100%" }}>
<Label>Hook Namespace Seed</Label> <label>Hook Namespace Seed</label>
<Input <Input
{...register("HookNamespace", { required: true })} {...register("HookNamespace", { required: true })}
autoComplete={"off"} autoComplete={"off"}
defaultValue={
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
}
/> />
{errors.HookNamespace?.type === "required" && ( {errors.HookNamespace?.type === "required" && (
<Box css={{ display: "inline", color: "$red11" }}> <Box css={{ display: "inline", color: "$red11" }}>
@@ -247,15 +165,14 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
</Box> </Box>
)} )}
<Box css={{ mt: "$3" }}> <Box css={{ mt: "$3" }}>
<Label>Hook Namespace (sha256)</Label> <label>Hook Namespace (sha256)</label>
<Input readOnly value={hashedNamespace} /> <Input readOnly value={hashedNamespace} />
</Box> </Box>
</Box> </Box>
<Box css={{ width: "100%" }}> <Box css={{ width: "100%" }}>
<Label style={{ marginBottom: "10px", display: "block" }}> <label style={{ marginBottom: "10px", display: "block" }}>
Hook parameters Hook parameters
</Label> </label>
<Stack> <Stack>
{fields.map((field, index) => ( {fields.map((field, index) => (
<Stack key={field.id}> <Stack key={field.id}>
@@ -295,81 +212,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
</Button> </Button>
</Stack> </Stack>
</Box> </Box>
<Box css={{ width: "100%", position: "relative" }}>
<Label>Fee</Label>
<Box css={{ display: "flex", alignItems: "center" }}>
<Input
type="number"
{...register("Fee", { required: true })}
autoComplete={"off"}
onKeyPress={e => {
if (e.key === "." || e.key === ",") {
e.preventDefault();
}
}}
step="1"
defaultValue={10000}
css={{
"-moz-appearance": "textfield",
"&::-webkit-outer-spin-button": {
"-webkit-appearance": "none",
margin: 0,
},
"&::-webkit-inner-spin-button ": {
"-webkit-appearance": "none",
margin: 0,
},
}}
/>
<Button
size="xs"
variant="primary"
outline
isLoading={estimateLoading}
css={{
position: "absolute",
right: "$2",
fontSize: "$xs",
cursor: "pointer",
alignContent: "center",
display: "flex",
}}
onClick={async e => {
e.preventDefault();
if (!account) return;
setEstimateLoading(true);
const formValues = getValues();
try {
const tx = await prepareDeployHookTx(
account,
formValues
);
if (tx) {
const res = await estimateFee(tx, account);
if (res && res.base_fee) {
setValue(
"Fee",
Math.round(
Number(res.base_fee || "")
).toString()
);
}
}
} catch (err) {}
setEstimateLoading(false);
}}
>
Suggest
</Button>
</Box>
{errors.Fee?.type === "required" && (
<Box css={{ display: "inline", color: "$red11" }}>
Fee is required
</Box>
)}
</Box>
{/* <Box css={{ width: "100%" }}> {/* <Box css={{ width: "100%" }}>
<label style={{ marginBottom: "10px", display: "block" }}> <label style={{ marginBottom: "10px", display: "block" }}>
Hook Grants Hook Grants
@@ -435,7 +277,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
isLoading={account?.isLoading} isLoading={account.isLoading}
> >
Set Hook Set Hook
</Button> </Button>
@@ -450,9 +292,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
} };
);
SetHookDialog.displayName = "SetHookDialog";
export default SetHookDialog; export default SetHookDialog;

View File

@@ -1,32 +0,0 @@
import { styled } from "../stitches.config";
import * as SwitchPrimitive from "@radix-ui/react-switch";
const StyledSwitch = styled(SwitchPrimitive.Root, {
all: "unset",
width: 42,
height: 25,
backgroundColor: "$mauve9",
borderRadius: "9999px",
position: "relative",
boxShadow: `0 2px 10px $colors$mauve2`,
WebkitTapHighlightColor: "rgba(0, 0, 0, 0)",
"&:focus": { boxShadow: `0 0 0 2px $colors$mauveA2` },
'&[data-state="checked"]': { backgroundColor: "$green11" },
});
const StyledThumb = styled(SwitchPrimitive.Thumb, {
display: "block",
width: 21,
height: 21,
backgroundColor: "white",
borderRadius: "9999px",
boxShadow: `0 2px 2px $colors$mauveA6`,
transition: "transform 100ms",
transform: "translateX(2px)",
willChange: "transform",
'&[data-state="checked"]': { transform: "translateX(19px)" },
});
// Exports
export const Switch = StyledSwitch;
export const SwitchThumb = StyledThumb;

View File

@@ -6,7 +6,7 @@ import React, {
useCallback, useCallback,
} from "react"; } from "react";
import type { ReactNode, ReactElement } from "react"; import type { ReactNode, ReactElement } from "react";
import { Box, Button, Flex, Input, Label, Stack, Text } from "."; import { Box, Button, Flex, Input, Stack, Text } from ".";
import { import {
Dialog, Dialog,
DialogTrigger, DialogTrigger,
@@ -29,26 +29,22 @@ interface TabProps {
children: ReactNode; children: ReactNode;
} }
// TODO customise messages shown // TODO customise strings shown
interface Props { interface Props {
label?: string;
activeIndex?: number; activeIndex?: number;
activeHeader?: string; activeHeader?: string;
headless?: boolean; headless?: boolean;
children: ReactElement<TabProps>[]; children: ReactElement<TabProps>[];
keepAllAlive?: boolean; keepAllAlive?: boolean;
defaultExtension?: string; defaultExtension?: string;
appendDefaultExtension?: boolean; forceDefaultExtension?: boolean;
allowedExtensions?: string[];
onCreateNewTab?: (name: string) => any; onCreateNewTab?: (name: string) => any;
onCloseTab?: (index: number, header?: string) => any; onCloseTab?: (index: number, header?: string) => any;
onChangeActive?: (index: number, header?: string) => any;
} }
export const Tab = (props: TabProps) => null; export const Tab = (props: TabProps) => null;
export const Tabs = ({ export const Tabs = ({
label = "Tab",
children, children,
activeIndex, activeIndex,
activeHeader, activeHeader,
@@ -56,13 +52,11 @@ export const Tabs = ({
keepAllAlive = false, keepAllAlive = false,
onCreateNewTab, onCreateNewTab,
onCloseTab, onCloseTab,
onChangeActive,
defaultExtension = "", defaultExtension = "",
appendDefaultExtension = false, forceDefaultExtension,
allowedExtensions,
}: Props) => { }: Props) => {
const [active, setActive] = useState(activeIndex || 0); const [active, setActive] = useState(activeIndex || 0);
const tabs: TabProps[] = children.map(elem => elem.props); const tabs: TabProps[] = children.map((elem) => elem.props);
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false); const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
const [tabname, setTabname] = useState(""); const [tabname, setTabname] = useState("");
@@ -74,9 +68,8 @@ export const Tabs = ({
useEffect(() => { useEffect(() => {
if (activeHeader) { if (activeHeader) {
const idx = tabs.findIndex(tab => tab.header === activeHeader); const idx = tabs.findIndex((tab) => tab.header === activeHeader);
if (idx !== -1) setActive(idx); setActive(idx);
else setActive(0);
} }
}, [activeHeader, tabs]); }, [activeHeader, tabs]);
@@ -87,33 +80,19 @@ export const Tabs = ({
const validateTabname = useCallback( const validateTabname = useCallback(
(tabname: string): { error: string | null } => { (tabname: string): { error: string | null } => {
if (tabs.find(tab => tab.header === tabname)) { if (tabs.find((tab) => tab.header === tabname)) {
return { error: "Name already exists." }; return { error: "Name already exists." };
} }
const ext = tabname.split(".").pop() || "";
if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: "This file extension is not allowed!" };
}
return { error: null }; return { error: null };
}, },
[allowedExtensions, tabs] [tabs]
);
const handleActiveChange = useCallback(
(idx: number, header?: string) => {
setActive(idx);
onChangeActive?.(idx, header);
},
[onChangeActive]
); );
const handleCreateTab = useCallback(() => { const handleCreateTab = useCallback(() => {
// add default extension in case omitted // add default extension in case omitted
let _tabname = tabname.includes(".") let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
? tabname if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
: `${tabname}.${defaultExtension}`; _tabname = _tabname + defaultExtension;
if (appendDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
} }
const chk = validateTabname(_tabname); const chk = validateTabname(_tabname);
@@ -124,20 +103,11 @@ export const Tabs = ({
setIsNewtabDialogOpen(false); setIsNewtabDialogOpen(false);
setTabname(""); setTabname("");
// switch to new tab?
setActive(tabs.length);
onCreateNewTab?.(_tabname); onCreateNewTab?.(_tabname);
}, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
// switch to new tab?
handleActiveChange(tabs.length, _tabname);
}, [
tabname,
defaultExtension,
appendDefaultExtension,
validateTabname,
onCreateNewTab,
handleActiveChange,
tabs.length,
]);
const handleCloseTab = useCallback( const handleCloseTab = useCallback(
(idx: number) => { (idx: number) => {
@@ -158,7 +128,7 @@ export const Tabs = ({
gap: "$3", gap: "$3",
flex: 1, flex: 1,
flexWrap: "nowrap", flexWrap: "nowrap",
marginBottom: "$2", marginBottom: "-1px",
width: "100%", width: "100%",
overflow: "auto", overflow: "auto",
}} }}
@@ -168,8 +138,8 @@ export const Tabs = ({
key={tab.header} key={tab.header}
role="tab" role="tab"
tabIndex={idx} tabIndex={idx}
onClick={() => handleActiveChange(idx, tab.header)} onClick={() => setActive(idx)}
onKeyPress={() => handleActiveChange(idx, tab.header)} onKeyPress={() => setActive(idx)}
outline={active !== idx} outline={active !== idx}
size="sm" size="sm"
css={{ css={{
@@ -216,17 +186,17 @@ export const Tabs = ({
size="sm" size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }} css={{ alignItems: "center", px: "$2", mr: "$3" }}
> >
<Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`} <Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle> <DialogTitle>Create new tab</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>{label} name</Label> <label>Tabname</label>
<Input <Input
value={tabname} value={tabname}
onChange={e => setTabname(e.target.value)} onChange={(e) => setTabname(e.target.value)}
onKeyPress={e => { onKeyPress={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
handleCreateTab(); handleCreateTab();
} }

View File

@@ -7,30 +7,20 @@ const Text = styled("span", {
variants: { variants: {
small: { small: {
true: { true: {
fontSize: "$xs", fontSize: '$xs'
}, }
}, },
muted: { muted: {
true: { true: {
color: "$mauve9", color: '$mauve9'
}, }
},
error: {
true: {
color: "$error",
},
}, },
monospace: { monospace: {
true: { true: {
fontFamily: "$monospace", fontFamily: '$monospace'
}, }
},
block: {
true: {
display: "block",
} }
} }
},
}); });
export default Text; export default Text;

View File

@@ -1,115 +0,0 @@
import { styled } from "../stitches.config";
export const Textarea = styled("textarea", {
// Reset
appearance: "none",
borderWidth: "0",
boxSizing: "border-box",
fontFamily: "inherit",
outline: "none",
width: "100%",
flex: "1",
backgroundColor: "$mauve4",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "$sm",
p: "$2",
fontSize: "$md",
lineHeight: 1,
color: "$mauve12",
boxShadow: `0 0 0 1px $colors$mauve8`,
WebkitTapHighlightColor: "rgba(0,0,0,0)",
"&::before": {
boxSizing: "border-box",
},
"&::after": {
boxSizing: "border-box",
},
fontVariantNumeric: "tabular-nums",
"&:-webkit-autofill": {
boxShadow: "inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3",
},
"&:-webkit-autofill::first-line": {
fontFamily: "$untitled",
color: "$mauve12",
},
"&:focus": {
boxShadow: `0 0 0 1px $colors$mauve10`,
"&:-webkit-autofill": {
boxShadow: `0 0 0 1px $colors$mauve10`,
},
},
"&::placeholder": {
color: "$mauve9",
},
"&:disabled": {
pointerEvents: "none",
backgroundColor: "$mauve2",
color: "$mauve8",
cursor: "not-allowed",
"&::placeholder": {
color: "$mauve7",
},
},
variants: {
variant: {
ghost: {
boxShadow: "none",
backgroundColor: "transparent",
"@hover": {
"&:hover": {
boxShadow: "inset 0 0 0 1px $colors$mauve7",
},
},
"&:focus": {
backgroundColor: "$loContrast",
boxShadow: `0 0 0 1px $colors$mauve10`,
},
"&:disabled": {
backgroundColor: "transparent",
},
"&:read-only": {
backgroundColor: "transparent",
},
},
deep: {
backgroundColor: "$deep",
boxShadow: "none",
},
},
state: {
invalid: {
boxShadow: "inset 0 0 0 1px $colors$crimson7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8",
},
},
valid: {
boxShadow: "inset 0 0 0 1px $colors$grass7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8",
},
},
},
cursor: {
default: {
cursor: "default",
"&:focus": {
cursor: "text",
},
},
text: {
cursor: "text",
},
},
},
});
export default Textarea;

View File

@@ -45,11 +45,11 @@ const StyledContent = styled(TooltipPrimitive.Content, {
}, },
".dark &": { ".dark &": {
boxShadow: boxShadow:
"0px 0px 10px 2px rgba(0,0,0,.45), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px", "0px 0px 10px 2px rgba(255,255,255,.15), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
}, },
".light &": { ".light &": {
boxShadow: boxShadow:
"0px 0px 10px 2px rgba(0,0,0,.25), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px", "0px 0px 10px 2px rgba(0,0,0,.15), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
}, },
}); });
@@ -64,15 +64,12 @@ interface ITooltip {
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
} }
const Tooltip: React.FC< const Tooltip: React.FC<ITooltip> = ({
React.ComponentProps<typeof StyledContent> & ITooltip
> = ({
children, children,
content, content,
open, open,
defaultOpen = false, defaultOpen = false,
onOpenChange, onOpenChange,
...rest
}) => { }) => {
return ( return (
<TooltipPrimitive.Root <TooltipPrimitive.Root
@@ -81,8 +78,8 @@ const Tooltip: React.FC<
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
> >
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger> <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<StyledContent side="bottom" align="center" {...rest}> <StyledContent side="bottom" align="center">
<div dangerouslySetInnerHTML={{ __html: content }} /> {content}
<StyledArrow offset={5} width={11} height={5} /> <StyledArrow offset={5} width={11} height={5} />
</StyledContent> </StyledContent>
</TooltipPrimitive.Root> </TooltipPrimitive.Root>

View File

@@ -1,228 +0,0 @@
import { Play } from "phosphor-react";
import { FC, useCallback, useEffect, useMemo } from "react";
import { useSnapshot } from "valtio";
import state from "../../state";
import {
modifyTransaction,
prepareState,
prepareTransaction,
TransactionState,
} from "../../state/transactions";
import { sendTransaction } from "../../state/actions";
import Box from "../Box";
import Button from "../Button";
import Flex from "../Flex";
import { TxJson } from "./json";
import { TxUI } from "./ui";
import { default as _estimateFee } from "../../utils/estimateFee";
import toast from 'react-hot-toast';
export interface TransactionProps {
header: string;
state: TransactionState;
}
const Transaction: FC<TransactionProps> = ({
header,
state: txState,
...props
}) => {
const { accounts, editorSettings } = useSnapshot(state);
const {
selectedAccount,
selectedTransaction,
txIsDisabled,
txIsLoading,
viewType,
editorSavedValue,
editorValue,
} = txState;
const setState = useCallback(
(pTx?: Partial<TransactionState>) => {
return modifyTransaction(header, pTx);
},
[header]
);
const prepareOptions = useCallback(
(state: TransactionState = txState) => {
const {
selectedTransaction,
selectedDestAccount,
selectedAccount,
txFields,
} = state;
const TransactionType = selectedTransaction?.value || null;
const Destination =
selectedDestAccount?.value ||
("Destination" in txFields ? null : undefined);
const Account = selectedAccount?.value || null;
return prepareTransaction({
...txFields,
TransactionType,
Destination,
Account,
});
},
[txState]
);
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = selectedAccount?.value;
if (!account || !transactionType || txIsLoading) {
setState({ txIsDisabled: true });
} else {
setState({ txIsDisabled: false });
}
}, [
selectedAccount?.value,
selectedTransaction?.value,
setState,
txIsLoading,
]);
const submitTest = useCallback(async () => {
let st: TransactionState | undefined;
const tt = txState.selectedTransaction?.value;
if (viewType === "json") {
// save the editor state first
const pst = prepareState(editorValue || "", tt);
if (!pst) return;
st = setState(pst);
}
const account = accounts.find(
acc => acc.address === selectedAccount?.value
);
if (txIsDisabled) return;
setState({ txIsLoading: true });
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
try {
if (!account) {
throw Error("Account must be selected from imported accounts!");
}
const options = prepareOptions(st);
if (options.Destination === null) {
throw Error("Destination account cannot be null");
}
await sendTransaction(account, options, { logPrefix });
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({
type: "error",
message: `${logPrefix}${error.message}`,
});
}
}
setState({ txIsLoading: false });
}, [
viewType,
accounts,
txIsDisabled,
setState,
header,
editorValue,
txState,
selectedAccount?.value,
prepareOptions,
]);
const resetState = useCallback(() => {
modifyTransaction(header, { viewType }, { replaceState: true });
}, [header, viewType]);
const jsonValue = useMemo(
() =>
editorSavedValue ||
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize),
[editorSavedValue, editorSettings.tabSize, prepareOptions]
);
const estimateFee = useCallback(
async (st?: TransactionState, opts?: { silent?: boolean }) => {
const state = st || txState;
const ptx = prepareOptions(state);
const account = accounts.find(
acc => acc.address === state.selectedAccount?.value
);
if (!account) {
if (!opts?.silent) {
toast.error("Please select account from the list.")
}
return
};
ptx.Account = account.address;
ptx.Sequence = account.sequence;
const res = await _estimateFee(ptx, account, opts);
const fee = res?.base_fee;
setState({ estimatedFee: fee });
return fee;
},
[accounts, prepareOptions, setState, txState]
);
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
{viewType === "json" ? (
<TxJson
value={jsonValue}
header={header}
state={txState}
setState={setState}
estimateFee={estimateFee}
/>
) : (
<TxUI state={txState} setState={setState} estimateFee={estimateFee} />
)}
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
mb: "$1",
}}
>
<Button
onClick={() => {
if (viewType === "ui") {
setState({ editorSavedValue: null, viewType: "json" });
} else setState({ viewType: "ui" });
}}
outline
>
{viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"}
</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>
);
};
export default Transaction;

View File

@@ -1,231 +0,0 @@
import Editor, { loader, useMonaco } from "@monaco-editor/react";
import { FC, useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import dark from "../../theme/editor/amy.json";
import light from "../../theme/editor/xcode_default.json";
import { useSnapshot } from "valtio";
import state, {
prepareState,
transactionsData,
TransactionState,
} from "../../state";
import Text from "../Text";
import Flex from "../Flex";
import { Link } from "..";
import { showAlert } from "../../state/actions/showAlert";
import { parseJSON } from "../../utils/json";
import { extractSchemaProps } from "../../utils/schema";
import amountSchema from "../../content/amount-schema.json";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
interface JsonProps {
value?: string;
header?: string;
setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState;
estimateFee?: () => Promise<string | undefined>;
}
export const TxJson: FC<JsonProps> = ({
value = "",
state: txState,
header,
setState,
}) => {
const { editorSettings, accounts } = useSnapshot(state);
const { editorValue = value, estimatedFee } = txState;
const { theme } = useTheme();
const [hasUnsaved, setHasUnsaved] = useState(false);
const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value
);
useEffect(() => {
setState({ editorValue: value });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
useEffect(() => {
const parsed = parseJSON(editorValue);
if (!parsed) return;
const tt = parsed.TransactionType;
const tx = transactionsData.find(t => t.TransactionType === tt);
if (tx) setCurrTxType(tx.TransactionType);
else {
setCurrTxType(undefined);
}
}, [editorValue]);
useEffect(() => {
if (editorValue === value) setHasUnsaved(false);
else setHasUnsaved(true);
}, [editorValue, value]);
const saveState = (value: string, transactionType?: string) => {
const tx = prepareState(value, transactionType);
if (tx) setState(tx);
};
const discardChanges = () => {
showAlert("Confirm", {
body: "Are you sure to discard these changes?",
confirmText: "Yes",
onConfirm: () => setState({ editorValue: value }),
});
};
const onExit = (value: string) => {
const options = parseJSON(value);
if (options) {
saveState(value, currTxType);
return;
}
showAlert("Error!", {
body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
confirmText: "Discard",
onConfirm: () => setState({ editorValue: value }),
onCancel: () => setState({ viewType: "json", editorSavedValue: value }),
});
};
const path = `file:///${header}`;
const monaco = useMonaco();
const getSchemas = useCallback(async (): Promise<any[]> => {
const txObj = transactionsData.find(
td => td.TransactionType === currTxType
);
let genericSchemaProps: any;
if (txObj) {
genericSchemaProps = extractSchemaProps(txObj);
} else {
genericSchemaProps = transactionsData.reduce(
(cumm, td) => ({
...cumm,
...extractSchemaProps(td),
}),
{}
);
}
return [
{
uri: "file:///main-schema.json", // id of the first schema
fileMatch: ["**.json"], // associate with our model
schema: {
title: header,
type: "object",
required: ["TransactionType", "Account"],
properties: {
...genericSchemaProps,
TransactionType: {
title: "Transaction Type",
enum: transactionsData.map(td => td.TransactionType),
},
Account: {
$ref: "file:///account-schema.json",
},
Destination: {
anyOf: [
{
$ref: "file:///account-schema.json",
},
{
type: "string",
title: "Destination Account",
},
],
},
Amount: {
$ref: "file:///amount-schema.json",
},
Fee: {
$ref: "file:///fee-schema.json",
},
},
},
},
{
uri: "file:///account-schema.json",
schema: {
type: "string",
title: "Account type",
enum: accounts.map(acc => acc.address),
},
},
{
uri: "file:///fee-schema.json",
schema: {
type: "string",
title: "Fee type",
const: estimatedFee,
description: estimatedFee
? "Above mentioned value is recommended base fee"
: undefined,
},
},
{
...amountSchema,
},
];
}, [accounts, currTxType, estimatedFee, header]);
useEffect(() => {
if (!monaco) return;
getSchemas().then(schemas => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas,
});
});
}, [getSchemas, monaco]);
return (
<Flex
fluid
column
css={{ height: "calc(100% - 45px)", position: "relative" }}
>
<Editor
className="hooks-editor"
language={"json"}
path={path}
height="100%"
beforeMount={monaco => {
monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme("light", light as any);
}}
value={editorValue}
onChange={val => setState({ editorValue: val })}
onMount={(editor, monaco) => {
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
});
// register onExit cb
const model = editor.getModel();
model?.onWillDispose(() => onExit(model.getValue()));
}}
theme={theme === "dark" ? "dark" : "light"}
/>
{hasUnsaved && (
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
This file has unsaved changes.{" "}
<Link onClick={() => saveState(editorValue, currTxType)}>save</Link>{" "}
<Link onClick={discardChanges}>discard</Link>
</Text>
)}
</Flex>
);
};

View File

@@ -1,320 +0,0 @@
import { FC, useCallback, useEffect, useState } from "react";
import Container from "../Container";
import Flex from "../Flex";
import Input from "../Input";
import Select from "../Select";
import Text from "../Text";
import {
SelectOption,
TransactionState,
transactionsData,
TxFields,
getTxFields,
} from "../../state/transactions";
import { useSnapshot } from "valtio";
import state from "../../state";
import { streamState } from "../DebugStream";
import { Button } from "..";
import Textarea from "../Textarea";
interface UIProps {
setState: (
pTx?: Partial<TransactionState> | undefined
) => TransactionState | undefined;
state: TransactionState;
estimateFee?: (...arg: any) => Promise<string | undefined>;
}
export const TxUI: FC<UIProps> = ({
state: txState,
setState,
estimateFee,
}) => {
const { accounts } = useSnapshot(state);
const {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
} = txState;
const transactionsOptions = transactionsData.map((tx) => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const accountOptions: SelectOption[] = accounts.map((acc) => ({
label: acc.name,
value: acc.address,
}));
const destAccountOptions: SelectOption[] = accounts
.map((acc) => ({
label: acc.name,
value: acc.address,
}))
.filter((acc) => acc.value !== selectedAccount?.value);
const [feeLoading, setFeeLoading] = useState(false);
const resetOptions = useCallback(
(tt: string) => {
const fields = getTxFields(tt);
if (!fields.Destination) setState({ selectedDestAccount: null });
return setState({ txFields: fields });
},
[setState]
);
const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc });
streamState.selectedAccount = acc;
};
const handleSetField = useCallback(
(field: keyof TxFields, value: string, opFields?: TxFields) => {
const fields = opFields || txFields;
const obj = fields[field];
setState({
txFields: {
...fields,
[field]: typeof obj === "object" ? { ...obj, $value: value } : value,
},
});
},
[setState, txFields]
);
const handleEstimateFee = useCallback(
async (state?: TransactionState, silent?: boolean) => {
setFeeLoading(true);
const fee = await estimateFee?.(state, { silent });
if (fee) handleSetField("Fee", fee, state?.txFields);
setFeeLoading(false);
},
[estimateFee, handleSetField]
);
const handleChangeTxType = (tt: SelectOption) => {
setState({ selectedTransaction: tt });
const newState = resetOptions(tt.value);
handleEstimateFee(newState, true);
};
const specialFields = ["TransactionType", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(
(k) => !specialFields.includes(k)
) as [keyof TxFields];
const switchToJson = () =>
setState({ editorSavedValue: null, viewType: "json" });
useEffect(() => {
const defaultOption = transactionsOptions.find(
(tt) => tt.value === "Payment"
);
if (defaultOption) {
handleChangeTxType(defaultOption);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto", pr: "$1" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<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: any) => handleChangeTxType(tt)}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/>
</Flex>
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<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: any) => setState({ selectedDestAccount: acc })}
/>
</Flex>
)}
{otherFields.map((field) => {
let _value = txFields[field];
let value: string | undefined;
if (typeof _value === "object") {
if (_value.$type === "json" && typeof _value.$value === "object") {
value = JSON.stringify(_value.$value, null, 2);
} else {
value = _value.$value.toString();
}
} else {
value = _value?.toString();
}
const isXrp = typeof _value === "object" && _value.$type === "xrp";
const isJson = typeof _value === "object" && _value.$type === "json";
const isFee = field === "Fee";
let rows = isJson
? (value?.match(/\n/gm)?.length || 0) + 1
: undefined;
if (rows && rows > 5) rows = 5;
return (
<Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
position: "relative",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isXrp ? " (XRP)" : "")}:{" "}
</Text>
{isJson ? (
<Textarea
rows={rows}
value={value}
spellCheck={false}
onChange={switchToJson}
css={{
width: "70%",
flex: "inherit",
resize: "vertical",
}}
/>
) : (
<Input
type={isFee ? "number" : "text"}
value={value}
onChange={(e) => {
if (isFee) {
const val = e.target.value
.replaceAll(".", "")
.replaceAll(",", "");
handleSetField(field, val);
} else {
handleSetField(field, e.target.value);
}
}}
onKeyPress={
isFee
? (e) => {
if (e.key === "." || e.key === ",") {
e.preventDefault();
}
}
: undefined
}
css={{
width: "70%",
flex: "inherit",
"-moz-appearance": "textfield",
"&::-webkit-outer-spin-button": {
"-webkit-appearance": "none",
margin: 0,
},
"&::-webkit-inner-spin-button ": {
"-webkit-appearance": "none",
margin: 0,
},
}}
/>
)}
{isFee && (
<Button
size="xs"
variant="primary"
outline
disabled={txState.txIsDisabled}
isDisabled={txState.txIsDisabled}
isLoading={feeLoading}
css={{
position: "absolute",
right: "$2",
fontSize: "$xs",
cursor: "pointer",
alignContent: "center",
display: "flex",
}}
onClick={() => handleEstimateFee()}
>
Suggest
</Button>
)}
</Flex>
</Flex>
);
})}
</Flex>
</Container>
);
};

View File

@@ -1,40 +0,0 @@
const Carbon = () => (
<svg
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 24V30"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
fill="#EDEDEF"
/>
</svg>
);
export default Carbon;

View File

@@ -1,75 +0,0 @@
const Firewall = () => (
<svg
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M33 13V7"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M27 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M39 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 25V19"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21 13H45"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21 19H45"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M45 7H21V25H45V7Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
fill="#EDEDEF"
/>
</svg>
);
export default Firewall;

View File

@@ -1,40 +0,0 @@
const Notary = () => (
<svg
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M37.5 10.5L26.5 21.5L21 16.0002"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M49 10.5L38 21.5L35.0784 18.5785"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 5L8.94099 16.0625L4.00543e-05 27.125H2.27587L10.5015 16.9475H16.5938V15.1775H10.5015L2.27582 5H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 5L57.059 16.0625L66 27.125H63.7241L55.4985 16.9475H49.4062V15.1775H55.4985L63.7242 5H66Z"
fill="#EDEDEF"
/>
</svg>
);
export default Notary;

View File

@@ -1,61 +0,0 @@
const Peggy = () => (
<svg
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M33 19C40.1797 19 46 16.3137 46 13C46 9.68629 40.1797 7 33 7C25.8203 7 20 9.68629 20 13C20 16.3137 25.8203 19 33 19Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 19V25"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M20 13V19C20 22 25 25 33 25C41 25 46 22 46 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M41 17.7633V23.7634"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M25 17.7633V23.7634"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
fill="#EDEDEF"
/>
</svg>
);
export default Peggy;

View File

@@ -1,40 +0,0 @@
const Starter = () => (
<svg
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M42 28H24C23.7347 28 23.4804 27.8946 23.2929 27.7071C23.1053 27.5196 23 27.2652 23 27V5C23 4.73479 23.1053 4.48044 23.2929 4.2929C23.4804 4.10537 23.7347 4.00001 24 4H36.0003L43 11V27C43 27.2652 42.8947 27.5196 42.7071 27.7071C42.5196 27.8946 42.2653 28 42 28V28Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M36 4V11H43.001"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
fill="#EDEDEF"
/>
</svg>
);
export default Starter;

View File

@@ -4,13 +4,14 @@ export { default as Container } from "./Container";
export { default as Heading } from "./Heading"; export { default as Heading } from "./Heading";
export { default as Stack } from "./Stack"; export { default as Stack } from "./Stack";
export { default as Text } from "./Text"; export { default as Text } from "./Text";
export { default as Input, Label } from "./Input"; export { default as Input } from "./Input";
export { default as Select } from "./Select"; export { default as Select } from "./Select";
export * from "./Tabs"; export * from "./Tabs";
export * from "./AlertDialog/primitive"; export * from "./AlertDialog";
export { default as Box } from "./Box"; export { default as Box } from "./Box";
export { default as Button } from "./Button"; export { default as Button } from "./Button";
export { default as Pre } from "./Pre"; export { default as Pre } from "./Pre";
export { default as ButtonGroup } from "./ButtonGroup"; export { default as ButtonGroup } from "./ButtonGroup";
export { default as DeployFooter } from "./DeployFooter";
export * from "./Dialog"; export * from "./Dialog";
export * from "./DropdownMenu"; export * from "./DropdownMenu";

View File

@@ -1,50 +0,0 @@
{
"uri": "file:///amount-schema.json",
"title": "Amount",
"description": "Specify xrp in drops and tokens as objects.",
"schema": {
"anyOf": [
{
"type": [
"number",
"string"
],
"exclusiveMinimum": 0,
"maximum": "100000000000000000"
},
{
"type": "object",
"properties": {
"currency": {
"description": "Arbitrary currency code for the token. Cannot be XRP."
},
"value": {
"type": [
"string",
"number"
],
"description": "Quoted decimal representation of the amount of the token."
},
"issuer": {
"type": "string",
"description": "Generally, the account that issues this token. In special cases, this can refer to the account that holds the token instead."
}
}
}
],
"defaultSnippets": [
{
"label": "Xrp",
"body": "1000000"
},
{
"label": "Token",
"body": {
"currency": "${1:13.1}",
"value": "${2:FOO}",
"description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
}
}
]
}
}

View File

@@ -27,8 +27,8 @@
"Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", "Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"TransactionType": "CheckCash", "TransactionType": "CheckCash",
"Amount": { "Amount": {
"$value": "100", "value": "100",
"$type": "xrp" "type": "currency"
}, },
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334", "CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12" "Fee": "12"
@@ -61,8 +61,8 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCreate", "TransactionType": "EscrowCreate",
"Amount": { "Amount": {
"$value": "100", "value": "100",
"$type": "xrp" "type": "currency"
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958, "CancelAfter": 533257958,
@@ -99,8 +99,8 @@
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX", "Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007", "TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": { "Amount": {
"$value": "100", "value": "100",
"$type": "xrp" "type": "currency"
}, },
"Flags": 1 "Flags": 1
}, },
@@ -122,8 +122,8 @@
"Sequence": 8, "Sequence": 8,
"TakerGets": "6000000", "TakerGets": "6000000",
"Amount": { "Amount": {
"$value": "100", "value": "100",
"$type": "xrp" "type": "currency"
} }
}, },
{ {
@@ -131,8 +131,8 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Amount": { "Amount": {
"$value": "100", "value": "100",
"$type": "xrp" "type": "currency"
}, },
"Fee": "12", "Fee": "12",
"Flags": 2147483648, "Flags": 2147483648,
@@ -142,8 +142,8 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelCreate", "TransactionType": "PaymentChannelCreate",
"Amount": { "Amount": {
"$value": "100", "value": "100",
"$type": "xrp" "type": "currency"
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400, "SettleDelay": 86400,
@@ -157,8 +157,8 @@
"TransactionType": "PaymentChannelFund", "TransactionType": "PaymentChannelFund",
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", "Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": { "Amount": {
"$value": "200", "value": "200",
"$type": "xrp" "type": "currency"
}, },
"Expiration": 543171558 "Expiration": 543171558
}, },
@@ -176,8 +176,8 @@
"Fee": "12", "Fee": "12",
"SignerQuorum": 3, "SignerQuorum": 3,
"SignerEntries": { "SignerEntries": {
"$type": "json", "type": "json",
"$value": [ "value": [
{ {
"SignerEntry": { "SignerEntry": {
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", "Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
@@ -213,8 +213,8 @@
"Flags": 262144, "Flags": 262144,
"LastLedgerSequence": 8007750, "LastLedgerSequence": 8007750,
"Amount": { "Amount": {
"$value": "100", "value": "100",
"$type": "xrp" "type": "currency"
}, },
"Sequence": 12 "Sequence": 12
} }

View File

@@ -12,20 +12,16 @@
"dependencies": { "dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1", "@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0", "@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.4.5", "@monaco-editor/react": "^4.4.1",
"@octokit/core": "^3.5.1", "@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7", "@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1", "@radix-ui/react-alert-dialog": "^0.1.1",
"@radix-ui/react-dialog": "^0.1.1", "@radix-ui/react-dialog": "^0.1.1",
"@radix-ui/react-dropdown-menu": "^0.1.1", "@radix-ui/react-dropdown-menu": "^0.1.1",
"@radix-ui/react-id": "^0.1.1", "@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6",
"@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7", "@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.6-0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"comment-parser": "^1.3.1",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"filesize": "^8.0.7", "filesize": "^8.0.7",
@@ -36,7 +32,6 @@
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"next": "^12.0.4", "next": "^12.0.4",
"next-auth": "^4.0.0-beta.5", "next-auth": "^4.0.0-beta.5",
"next-plausible": "^3.2.0",
"next-themes": "^0.1.1", "next-themes": "^0.1.1",
"normalize-url": "^7.0.2", "normalize-url": "^7.0.2",
"octokit": "^1.7.0", "octokit": "^1.7.0",

View File

@@ -7,7 +7,6 @@ import { ThemeProvider } from "next-themes";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { IdProvider } from "@radix-ui/react-id"; import { IdProvider } from "@radix-ui/react-id";
import PlausibleProvider from "next-plausible";
import { darkTheme, css } from "../stitches.config"; import { darkTheme, css } from "../stitches.config";
import Navigation from "../components/Navigation"; import Navigation from "../components/Navigation";
@@ -17,9 +16,6 @@ import state from "../state";
import TimeAgo from "javascript-time-ago"; import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en.json"; import en from "javascript-time-ago/locale/en.json";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import Alert from "../components/AlertDialog";
import { Button, Flex } from "../components";
import { ChatCircleText } from "phosphor-react";
TimeAgo.setDefaultLocale(en.locale); TimeAgo.setDefaultLocale(en.locale);
TimeAgo.addLocale(en); TimeAgo.addLocale(en);
@@ -40,7 +36,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
if ( if (
!gistId && !gistId &&
router.isReady && router.isReady &&
router.pathname.includes("/develop") && !router.pathname.includes("/sign-in") &&
!snap.files.length && !snap.files.length &&
!snap.mainModalShowed !snap.mainModalShowed
) { ) {
@@ -64,22 +60,22 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
<meta property="og:url" content={`${origin}${router.asPath}`} /> <meta property="og:url" content={`${origin}${router.asPath}`} />
<title>XRPL Hooks Builder</title> <title>XRPL Hooks Editor</title>
<meta property="og:title" content="XRPL Hooks Builder" /> <meta property="og:title" content="XRPL Hooks Editor" />
<meta name="twitter:title" content="XRPL Hooks Builder" /> <meta name="twitter:title" content="XRPL Hooks Editor" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@XRPLF" /> <meta name="twitter:site" content="@xrpllabs" />
<meta <meta
name="description" name="description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger." content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
/> />
<meta <meta
property="og:description" property="og:description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger." content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
/> />
<meta <meta
name="twitter:description" name="twitter:description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger." content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger.."
/> />
<meta property="og:image" content={`${origin}${shareImg}`} /> <meta property="og:image" content={`${origin}${shareImg}`} />
<meta property="og:image:width" content="1200" /> <meta property="og:image:width" content="1200" />
@@ -104,7 +100,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
/> />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
<meta name="application-name" content="XRPL Hooks Builder" /> <meta name="application-name" content="XRPL Hooks Editor" />
<meta name="msapplication-TileColor" content="#c10ad0" /> <meta name="msapplication-TileColor" content="#c10ad0" />
<meta <meta
name="theme-color" name="theme-color"
@@ -117,7 +113,6 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
media="(prefers-color-scheme: light)" media="(prefers-color-scheme: light)"
/> />
</Head> </Head>
<IdProvider> <IdProvider>
<SessionProvider session={session}> <SessionProvider session={session}>
<ThemeProvider <ThemeProvider
@@ -128,10 +123,6 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
light: "light", light: "light",
dark: darkTheme.className, dark: darkTheme.className,
}} }}
>
<PlausibleProvider
domain="hooks-builder.xrpl.org"
trackOutboundLinks
> >
<Navigation /> <Navigation />
<Component {...pageProps} /> <Component {...pageProps} />
@@ -149,20 +140,6 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
})(), })(),
}} }}
/> />
<Alert />
<Flex
as="a"
href="https://github.com/XRPLF/Hooks/discussions"
target="_blank"
rel="noopener noreferrer"
css={{ position: "fixed", right: "$4", bottom: "$4" }}
>
<Button size="sm" variant="primary" outline>
<ChatCircleText size={14} style={{ marginRight: "0px" }} />
Bugs & Discussions
</Button>
</Flex>
</PlausibleProvider>
</ThemeProvider> </ThemeProvider>
</SessionProvider> </SessionProvider>
</IdProvider> </IdProvider>

View File

@@ -1,18 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const { url, opts } = req.body
const r = await fetch(url, opts);
if (!r.ok) throw (r.statusText)
const data = await r.json()
return res.json(data)
} catch (error) {
console.warn(error)
return res.status(500).json({ message: "Something went wrong!" })
}
}

View File

@@ -1,19 +1,15 @@
import { Label } from "@radix-ui/react-label";
import type { NextPage } from "next"; import type { NextPage } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { FileJs, Gear, Play } from "phosphor-react"; import { Play } from "phosphor-react";
import Hotkeys from "react-hot-keys"; import Hotkeys from "react-hot-keys";
import Split from "react-split"; import Split from "react-split";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { ButtonGroup, Flex } from "../../components";
import Box from "../../components/Box"; import Box from "../../components/Box";
import Button from "../../components/Button"; import Button from "../../components/Button";
import Popover from "../../components/Popover";
import RunScript from "../../components/RunScript";
import state from "../../state"; import state from "../../state";
import { compileCode } from "../../state/actions"; import { compileCode } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { styled } from "../../stitches.config";
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), { const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
ssr: false, ssr: false,
@@ -23,128 +19,6 @@ const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false, ssr: false,
}); });
const OptimizationText = () => (
<span>
Specify which optimization level to use for compiling. For example -O0 means
no optimization: this level compiles the fastest and generates the most
debuggable code. -O2 means moderate level of optimization which enables most
optimizations. Read more about the options from{" "}
<a
className="link"
rel="noopener noreferrer"
target="_blank"
href="https://clang.llvm.org/docs/CommandGuide/clang.html#cmdoption-o0"
>
clang documentation
</a>
.
</span>
);
const StyledOptimizationText = styled(OptimizationText, {
color: "$mauve12 !important",
fontSize: "200px",
"span a.link": {
color: "red",
},
});
const CompilerSettings = () => {
const snap = useSnapshot(state);
return (
<Flex css={{ minWidth: 200, flexDirection: "column", gap: "$5" }}>
<Box>
<Label
style={{
flexDirection: "row",
display: "flex",
}}
>
Optimization level{" "}
<Popover
css={{
maxWidth: "240px",
lineHeight: "1.3",
a: {
color: "$purple11",
},
".dark &": {
backgroundColor: "$black !important",
".arrow": {
fill: "$colors$black",
},
},
}}
content={<StyledOptimizationText />}
>
<Flex
css={{
position: "relative",
top: "-1px",
ml: "$1",
backgroundColor: "$mauve8",
borderRadius: "$full",
cursor: "pointer",
width: "16px",
height: "16px",
alignItems: "center",
justifyContent: "center",
}}
>
?
</Flex>
</Popover>
</Label>
<ButtonGroup css={{ mt: "$2", fontFamily: "$monospace" }}>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O0"}
onClick={() => (state.compileOptions.optimizationLevel = "-O0")}
>
-O0
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O1"}
onClick={() => (state.compileOptions.optimizationLevel = "-O1")}
>
-O1
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O2"}
onClick={() => (state.compileOptions.optimizationLevel = "-O2")}
>
-O2
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O3"}
onClick={() => (state.compileOptions.optimizationLevel = "-O3")}
>
-O3
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O4"}
onClick={() => (state.compileOptions.optimizationLevel = "-O4")}
>
-O4
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-Os"}
onClick={() => (state.compileOptions.optimizationLevel = "-Os")}
>
-Os
</Button>
</ButtonGroup>
</Box>
</Flex>
);
};
const Home: NextPage = () => { const Home: NextPage = () => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
@@ -160,24 +34,13 @@ const Home: NextPage = () => {
> >
<main style={{ display: "flex", flex: 1, position: "relative" }}> <main style={{ display: "flex", flex: 1, position: "relative" }}>
<HooksEditor /> <HooksEditor />
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() === {snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
"c" && ( "c" && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
onKeyDown={() => onKeyDown={() =>
!snap.compiling && snap.files.length && compileCode(snap.active) !snap.compiling && snap.files.length && compileCode(snap.active)
} }
>
<Flex
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
gap: "$2",
}}
> >
<Button <Button
variant="primary" variant="primary"
@@ -185,27 +48,6 @@ const Home: NextPage = () => {
disabled={!snap.files.length} disabled={!snap.files.length}
isLoading={snap.compiling} isLoading={snap.compiling}
onClick={() => compileCode(snap.active)} onClick={() => compileCode(snap.active)}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
<Popover content={<CompilerSettings />}>
<Button variant="primary" css={{ px: "10px" }}>
<Gear size="16px" />
</Button>
</Popover>
</Flex>
</Hotkeys>
)}
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"js" && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() =>
!snap.compiling && snap.files.length && compileCode(snap.active)
}
>
<Flex
css={{ css={{
position: "absolute", position: "absolute",
bottom: "$4", bottom: "$4",
@@ -213,21 +55,19 @@ const Home: NextPage = () => {
alignItems: "center", alignItems: "center",
display: "flex", display: "flex",
cursor: "pointer", cursor: "pointer",
gap: "$2",
}} }}
> >
<RunScript file={snap.files[snap.active]} /> <Play weight="bold" size="16px" />
</Flex> Compile to Wasm
</Button>
</Hotkeys> </Hotkeys>
)} )}
</main> </main>
<Flex css={{ width: "100%" }}> <Box
<Flex
css={{ css={{
flex: 1, display: "flex",
background: "$mauve1", background: "$mauve1",
position: "relative", position: "relative",
borderRight: "1px solid $mauve8",
}} }}
> >
<LogBox <LogBox
@@ -235,23 +75,7 @@ const Home: NextPage = () => {
clearLog={() => (state.logs = [])} clearLog={() => (state.logs = [])}
logs={snap.logs} logs={snap.logs}
/> />
</Flex> </Box>
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"js" && (
<Flex
css={{
flex: 1,
}}
>
<LogBox
Icon={FileJs}
title="Script Log"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
/>
</Flex>
)}
</Flex>
</Split> </Split>
); );
}; };

View File

@@ -1,14 +1,23 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Play } from "phosphor-react";
import { FC, useCallback, useEffect, useState } from "react";
import Split from "react-split"; import Split from "react-split";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { Box, Container, Flex, Tab, Tabs } from "../../components"; import {
import Transaction from "../../components/Transaction"; Box,
Button,
Container,
Flex,
Input,
Select,
Tab,
Tabs,
Text,
} from "../../components";
import transactionsData from "../../content/transactions.json";
import state from "../../state"; import state from "../../state";
import { sendTransaction } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTransaction } from "../../state";
import { useEffect, useState } from "react";
import { FileJs } from "phosphor-react";
import RunScript from '../../components/RunScript';
const DebugStream = dynamic(() => import("../../components/DebugStream"), { const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false, ssr: false,
@@ -21,47 +30,353 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
ssr: false, ssr: false,
}); });
const Test = () => { // type SelectOption<T> = { value: T, label: string };
// This and useEffect is here to prevent useLayoutEffect warnings from react-split type TxFields = Omit<
const [showComponent, setShowComponent] = useState(false); typeof transactionsData[0],
const { transactionLogs } = useSnapshot(state); "Account" | "Sequence" | "TransactionType"
const { transactions, activeHeader } = useSnapshot(transactionsState); >;
const snap = useSnapshot(state); type OtherFields = (keyof Omit<TxFields, "Destination">)[];
useEffect(() => {
setShowComponent(true);
}, []);
if (!showComponent) {
return null;
}
const hasScripts = Boolean(
snap.files.filter(f => f.name.toLowerCase()?.endsWith(".js")).length
);
const renderNav = () => ( interface Props {
<Flex css={{ gap: "$3" }}> header?: string;
{snap.files }
.filter(f => f.name.endsWith(".js"))
.map(file => ( const Transaction: FC<Props> = ({ header, ...props }) => {
<RunScript file={file} key={file.name} /> 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];
}
});
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
await sendTransaction(
account,
{
TransactionType,
...options,
},
{ logPrefix }
);
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({ type: "error", message: error.message });
}
}
setTxIsLoading(false);
}, [
header,
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
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)" }} {...props}>
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<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",
pr: "1px",
}}
>
<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",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={(e) =>
setTxFields({
...txFields,
Amount: { type: "currency", value: e.target.value },
})
}
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<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",
pr: "1px",
}}
>
<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,
})
}
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex> </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 = () => {
const { transactionLogs } = useSnapshot(state);
const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
return ( return (
<Container css={{ px: 0 }}> <Container css={{ px: 0 }}>
<Split <Split
direction="vertical" direction="vertical"
sizes={ sizes={getSplit("testVertical") || [50, 50]}
hasScripts && getSplit("testVertical")?.length === 2
? [50, 20, 30]
: hasScripts
? [50, 20, 50]
: [50, 50]
}
gutterSize={4} gutterSize={4}
gutterAlign="center" gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }} style={{ height: "calc(100vh - 60px)" }}
onDragEnd={e => saveSplit("testVertical", e)} onDragEnd={(e) => saveSplit("testVertical", e)}
> >
<Flex <Flex
row row
@@ -83,27 +398,23 @@ const Test = () => {
width: "100%", width: "100%",
height: "100%", height: "100%",
}} }}
onDragEnd={e => saveSplit("testHorizontal", e)} onDragEnd={(e) => saveSplit("testHorizontal", e)}
> >
<Box css={{ width: "55%", px: "$2" }}> <Box css={{ width: "55%", px: "$2" }}>
<Tabs <Tabs
label="Transaction"
activeHeader={activeHeader}
// TODO make header a required field
onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header;
}}
keepAllAlive keepAllAlive
defaultExtension="json" forceDefaultExtension
allowedExtensions={["json"]} defaultExtension=".json"
onCreateNewTab={header => modifyTransaction(header, {})} onCreateNewTab={(name) =>
onCloseTab={(idx, header) => setTabHeaders(tabHeaders.concat(name))
header && modifyTransaction(header, undefined) }
onCloseTab={(index) =>
setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))
} }
> >
{transactions.map(({ header, state }) => ( {tabHeaders.map((header) => (
<Tab key={header} header={header}> <Tab key={header} header={header}>
<Transaction state={state} header={header} /> <Transaction header={header} />
</Tab> </Tab>
))} ))}
</Tabs> </Tabs>
@@ -113,25 +424,8 @@ const Test = () => {
</Box> </Box>
</Split> </Split>
</Flex> </Flex>
{hasScripts ? (
<Flex <Flex row fluid>
as="div"
css={{
borderTop: "1px solid $mauve6",
background: "$mauve1",
flexDirection: "column",
}}
>
<LogBox
Icon={FileJs}
title="Helper scripts"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
renderNav={renderNav}
/>
</Flex>
) : null}
<Flex>
<Split <Split
direction="horizontal" direction="horizontal"
sizes={[50, 50]} sizes={[50, 50]}

View File

@@ -35,11 +35,9 @@ export const compileCode = async (activeId: number) => {
body: JSON.stringify({ body: JSON.stringify({
output: "wasm", output: "wasm",
compress: true, compress: true,
strip: state.compileOptions.strip,
files: [ files: [
{ {
type: "c", type: "c",
options: state.compileOptions.optimizationLevel || '-O2',
name: state.files[activeId].name, name: state.files[activeId].name,
src: state.files[activeId].content, src: state.files[activeId].content,
}, },

View File

@@ -4,22 +4,19 @@ import toast from "react-hot-toast";
import state, { IAccount } from "../index"; import state, { IAccount } from "../index";
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator"; import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
import { SetHookData } from "../../components/SetHookDialog"; import { SetHookData } from "../../components/SetHookDialog";
import { Link } from "../../components";
import { ref } from "valtio";
import estimateFee from "../../utils/estimateFee";
export const sha256 = async (string: string) => { export const sha256 = async (string: string) => {
const utf8 = new TextEncoder().encode(string); const utf8 = new TextEncoder().encode(string);
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8); const hashBuffer = await crypto.subtle.digest('SHA-256', utf8);
const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, "0")) .map((bytes) => bytes.toString(16).padStart(2, '0'))
.join(""); .join('');
return hashHex; return hashHex;
}; }
function toHex(str: string) { function toHex(str: string) {
var result = ""; var result = '';
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16); result += str.charCodeAt(i).toString(16);
} }
@@ -50,36 +47,29 @@ function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
return result; return result;
} }
export const prepareDeployHookTx = async ( /* deployHook function turns the wasm binary into
account: IAccount & { name?: string }, * hex string, signs the transaction and deploys it to
data: SetHookData * Hooks testnet.
) => { */
const activeFile = state.files[state.active]?.compiledContent export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
? state.files[state.active] if (
: state.files.filter((file) => file.compiledContent)[0]; !state.files ||
state.files.length === 0 ||
if (!state.files || state.files.length === 0) { !state.files?.[state.active]?.compiledContent
) {
return; return;
} }
if (!activeFile?.compiledContent) { if (!state.files?.[state.active]?.compiledContent) {
return; return;
} }
if (!state.client) { if (!state.client) {
return; return;
} }
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase(); const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
const hookOnValues: (keyof TTS)[] = data.Invoke.map((tt) => tt.value); const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value);
const { HookParameters } = data; const { HookParameters } = data;
const filteredHookParameters = HookParameters.filter( const filteredHookParameters = HookParameters.filter(hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue)?.map(aa => ({ HookParameter: { HookParameterName: toHex(aa.HookParameter.HookParameterName || ''), HookParameterValue: aa.HookParameter.HookParameterValue || '' } }));
(hp) =>
hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
)?.map((aa) => ({
HookParameter: {
HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
HookParameterValue: aa.HookParameter.HookParameterValue || "",
},
}));
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => { // const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
// return { // return {
// HookGrant: { // HookGrant: {
@@ -89,56 +79,31 @@ export const prepareDeployHookTx = async (
// } // }
// } // }
// }); // });
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const tx = { const tx = {
Account: account.address, Account: account.address,
TransactionType: "SetHook", TransactionType: "SetHook",
Sequence: account.sequence, Sequence: account.sequence,
Fee: data.Fee, Fee: "100000",
Hooks: [ Hooks: [
{ {
Hook: { Hook: {
CreateCode: arrayBufferToHex( CreateCode: arrayBufferToHex(
activeFile?.compiledContent state.files?.[state.active]?.compiledContent
).toUpperCase(), ).toUpperCase(),
HookOn: calculateHookOn(hookOnValues), HookOn: calculateHookOn(hookOnValues),
HookNamespace, HookNamespace,
HookApiVersion: 0, HookApiVersion: 0,
Flags: 1, Flags: 1,
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }), // ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
...(filteredHookParameters.length > 0 && { ...(filteredHookParameters.length > 0 && { HookParameters: filteredHookParameters }),
HookParameters: filteredHookParameters, }
}), }
}, ]
},
],
}; };
return tx;
}
};
/* 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 },
data: SetHookData
) => {
if (typeof window !== "undefined") {
const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active]
: state.files.filter((file) => file.compiledContent)[0];
state.deployValues[activeFile.name] = data;
const tx = await prepareDeployHookTx(account, data);
if (!tx) {
return;
}
if (!state.client) {
return;
}
const keypair = derive.familySeed(account.secret); const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair); const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find( const currentAccount = state.accounts.find(
(acc) => acc.address === account.address (acc) => acc.address === account.address
@@ -147,9 +112,8 @@ export const deployHook = async (
currentAccount.isLoading = true; currentAccount.isLoading = true;
} }
let submitRes; let submitRes;
try { try {
submitRes = await state.client?.send({ submitRes = await state.client.send({
command: "submit", command: "submit",
tx_blob: signedTransaction, tx_blob: signedTransaction,
}); });
@@ -161,28 +125,12 @@ export const deployHook = async (
}); });
state.deployLogs.push({ state.deployLogs.push({
type: "success", type: "success",
message: ref( message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
<>
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
Transaction hash:{" "}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`}
target="_blank"
rel="noopener noreferrer"
>
{submitRes.tx_json?.hash}
</Link>
</>
),
// message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
}); });
} else { } else {
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${ message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`,
submitRes.engine_result_message || submitRes.error_exception
}`,
}); });
} }
} catch (err) { } catch (err) {
@@ -207,7 +155,7 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
(acc) => acc.address === account.address (acc) => acc.address === account.address
); );
if (currentAccount?.isLoading || !currentAccount?.hooks.length) { if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
return; return
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const tx = { const tx = {
@@ -220,20 +168,12 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
Hook: { Hook: {
CreateCode: "", CreateCode: "",
Flags: 1, Flags: 1,
}, }
}, }
], ]
}; };
const keypair = derive.familySeed(account.secret); const keypair = derive.familySeed(account.secret);
try {
// Update tx Fee value with network estimation
const res = await estimateFee(tx, account);
tx["Fee"] = res?.base_fee ? res?.base_fee : "1000";
} catch (err) {
// use default value what you defined earlier
console.log(err);
}
const { signedTransaction } = sign(tx, keypair); const { signedTransaction } = sign(tx, keypair);
if (currentAccount) { if (currentAccount) {
@@ -248,7 +188,7 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
}); });
if (submitRes.engine_result === "tesSUCCESS") { if (submitRes.engine_result === "tesSUCCESS") {
toast.success("Hook deleted successfully ✅", { id: toastId }); toast.success('Hook deleted successfully ✅', { id: toastId })
state.deployLogs.push({ state.deployLogs.push({
type: "success", type: "success",
message: "Hook deleted successfully ✅", message: "Hook deleted successfully ✅",
@@ -259,20 +199,15 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
}); });
currentAccount.hooks = []; currentAccount.hooks = [];
} else { } else {
toast.error( toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, { id: toastId })
`${submitRes.engine_result_message || submitRes.error_exception}`,
{ id: toastId }
);
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${ message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`,
submitRes.engine_result_message || submitRes.error_exception
}`,
}); });
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
toast.error("Error occured while deleting hoook", { id: toastId }); toast.error('Error occured while deleting hoook', { id: toastId })
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: "Error occured while deleting hook", message: "Error occured while deleting hook",

View File

@@ -8,8 +8,7 @@ export const downloadAsZip = async () => {
state.zipLoading = true state.zipLoading = true
// TODO do something about file/gist loading state // TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content })); const files = state.files.map(({ name, content }) => ({ name, content }));
const wasmFiles = state.files.filter(i => i.compiledContent).map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent })); const zipped = await createZip(files);
const zipped = await createZip([...files, ...wasmFiles]);
const zipFileName = guessZipFileName(files); const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName); zipped.saveFile(zipFileName);
} catch (error) { } catch (error) {

View File

@@ -19,22 +19,20 @@ export const fetchFiles = (gistId: string) => {
octokit octokit
.request("GET /gists/{gist_id}", { gist_id: gistId }) .request("GET /gists/{gist_id}", { gist_id: gistId })
.then(async res => { .then(async res => {
if (!Object.values(templateFileIds).map(v => v.id).includes(gistId)) { if (!Object.values(templateFileIds).includes(gistId)) {
return res return res
} }
// in case of templates, fetch header file(s) and append to res // in case of templates, fetch header file(s) and append to res
let resHeaderJson;
try { try {
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`); const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
if (resHeader.ok) { if (resHeader.ok) {
const resHeaderJson = await resHeader.json() resHeaderJson = await resHeader.json();
const headerFiles: Record<string, { filename: string; content: string; language: string }> = {};
Object.entries(resHeaderJson).forEach(([key, value]) => {
const fname = `${key}.h`;
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
})
const files = { const files = {
...res.data.files, ...res.data.files,
...headerFiles 'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: resHeaderJson.hookapi, language: 'C' },
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: resHeaderJson.hookmacro, language: 'C' },
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: resHeaderJson.sfcodes, language: 'C' },
}; };
res.data.files = files; res.data.files = files;
} }
@@ -60,29 +58,6 @@ export const fetchFiles = (gistId: string) => {
language: res.data.files?.[filename]?.language?.toLowerCase() || "", language: res.data.files?.[filename]?.language?.toLowerCase() || "",
content: res.data.files?.[filename]?.content || "", content: res.data.files?.[filename]?.content || "",
})); }));
// Sort files so that the source files are first
// In case of other files leave the order as it its
files.sort((a, b) => {
const aBasename = a.name.split('.')?.[0];
const aCext = a.name?.toLowerCase().endsWith('.c');
const bBasename = b.name.split('.')?.[0];
const bCext = b.name?.toLowerCase().endsWith('.c');
// If a has c extension and b doesn't move a up
if (aCext && !bCext) {
return -1;
}
if (!aCext && bCext) {
return 1
}
// Otherwise fallback to default sorting based on basename
if (aBasename > bBasename) {
return 1;
}
if (bBasename > aBasename) {
return -1;
}
return 0;
})
state.loading = false; state.loading = false;
if (files.length > 0) { if (files.length > 0) {
state.logs.push({ state.logs.push({

View File

@@ -15,13 +15,3 @@ export const saveFile = (showToast: boolean = true) => {
toast.success("Saved successfully", { position: "bottom-center" }); toast.success("Saved successfully", { position: "bottom-center" });
} }
}; };
export const saveAllFiles = () => {
const editorModels = state.editorCtx?.getModels();
state.files.forEach(file => {
const currentModel = editorModels?.find(model => model.uri.path.endsWith('/' + file.name))
if (currentModel) {
file.content = currentModel?.getValue() || '';
}
})
}

View File

@@ -20,11 +20,14 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
const { Fee = "1000", ...opts } = txOptions const { Fee = "1000", ...opts } = txOptions
const tx: TransactionOptions = { const tx: TransactionOptions = {
Account: account.address, Account: account.address,
Sequence: account.sequence, Sequence: account.sequence, // TODO auto-fillable
Fee, // TODO auto-fillable default Fee, // TODO auto-fillable
...opts ...opts
}; };
const currAcc = state.accounts.find(acc => acc.address === account.address);
if (currAcc) {
currAcc.sequence = account.sequence + 1;
}
const { logPrefix = '' } = options || {} const { logPrefix = '' } = options || {}
try { try {
const signedAccount = derive.familySeed(account.secret); const signedAccount = derive.familySeed(account.secret);
@@ -44,10 +47,6 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`, message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
}); });
} }
const currAcc = state.accounts.find(acc => acc.address === account.address);
if (currAcc && response.account_sequence_next) {
currAcc.sequence = response.account_sequence_next;
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
state.transactionLogs.push({ state.transactionLogs.push({

View File

@@ -1,23 +0,0 @@
import { ref } from 'valtio';
import { AlertState, alertState } from "../../components/AlertDialog";
export const showAlert = (title: string, opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}) => {
const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts
const body = (_body && typeof _body === 'object') ? ref(_body) : _body
const confirmPrefix = (_confirmPrefix && typeof _confirmPrefix === 'object') ? ref(_confirmPrefix) : _confirmPrefix
const nwState: AlertState = {
isOpen: true,
title,
body,
confirmPrefix,
cancelText: undefined,
confirmText: undefined,
onCancel: undefined,
onConfirm: undefined,
...rest,
}
Object.entries(nwState).forEach(([key, value]) => {
(alertState as any)[key] = value
})
}

View File

@@ -4,7 +4,6 @@ import { Octokit } from "@octokit/core";
import Router from "next/router"; import Router from "next/router";
import state from '../index'; import state from '../index';
import { saveAllFiles } from "./saveFile";
const octokit = new Octokit(); const octokit = new Octokit();
@@ -13,7 +12,6 @@ export const syncToGist = async (
session?: Session | null, session?: Session | null,
createNewGist?: boolean createNewGist?: boolean
) => { ) => {
saveAllFiles();
let files: Record<string, { filename: string; content: string }> = {}; let files: Record<string, { filename: string; content: string }> = {};
state.gistLoading = true; state.gistLoading = true;

View File

@@ -1,41 +1,20 @@
import Carbon from "../../components/icons/Carbon"; // export const templateFileIds = {
import Firewall from "../../components/icons/Firewall"; // 'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept
import Notary from "../../components/icons/Notary"; // 'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26',
import Peggy from "../../components/icons/Peggy"; // 'notary': 'a789c75f591eeab7932fd702ed8cf9ea',
import Starter from "../../components/icons/Starter"; // 'carbon': '43925143fa19735d8c6505c34d3a6a47',
// 'peggy': 'ceaf352e2a65741341033ab7ef05c448',
// 'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
// }
export const templateFileIds = { export const templateFileIds = {
'starter': { 'starter': '1f7d2963d9e342ea092286115274f3e3',
id: '9106f1fe60482d90475bfe8f1315affe', 'firewall': '70edec690f0de4dd315fad1f4f996d8c',
name: 'Starter', 'notary': '3d5677768fe8a54c4f6317e185d9ba66',
description: 'Just a basic starter with essential imports, just accepts any transaction coming through', 'carbon': 'a9fbcaf1b816b198c7fc0f62962bebf2',
icon: Starter 'doubler': '56b86174aeb70b2b48eee962bad3e355',
'peggy': 'd21298a37e1550b781682014762a567b',
}, 'headers': '55f639bce59a49c58c45e663776b5138'
'firewall': {
id: '1cc30f39c8a0b9c55b88c312669ca45e', // Forked
name: 'Firewall',
description: 'This Hook essentially checks a blacklist of accounts',
icon: Firewall
},
'notary': {
id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked
name: 'Notary',
description: 'Collecting signatures for multi-sign transactions',
icon: Notary
},
'carbon': {
id: '5941c19dce3e147948f564e224553c02',
name: 'Carbon',
description: 'Send a percentage of sum to an address',
icon: Carbon
},
'peggy': {
id: '049784a83fa068faf7912f663f7b6471', // Forked
name: 'Peggy',
description: 'An oracle based stable coin hook',
icon: Peggy
},
} }
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h']; export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'hookmacro.h']

View File

@@ -35,25 +35,18 @@ export interface IAccount {
hooks: string[]; hooks: string[];
isLoading: boolean; isLoading: boolean;
version?: string; version?: string;
error?: {
message: string;
code: string;
} | null;
} }
export interface ILog { export interface ILog {
type: "error" | "warning" | "log" | "success"; type: "error" | "warning" | "log" | "success";
message: string | JSX.Element; message: string;
key?: string;
jsonData?: any, jsonData?: any,
timestring?: string; timestamp?: string;
link?: string; link?: string;
linkText?: string; linkText?: string;
defaultCollapsed?: boolean defaultCollapsed?: boolean
} }
export type DeployValue = Record<IFile['name'], any>;
export interface IState { export interface IState {
files: IFile[]; files: IFile[];
gistId?: string | null; gistId?: string | null;
@@ -68,7 +61,6 @@ export interface IState {
logs: ILog[]; logs: ILog[];
deployLogs: ILog[]; deployLogs: ILog[];
transactionLogs: ILog[]; transactionLogs: ILog[];
scriptLogs: ILog[];
editorCtx?: typeof monaco.editor; editorCtx?: typeof monaco.editor;
editorSettings: { editorSettings: {
tabSize: number; tabSize: number;
@@ -81,11 +73,6 @@ export interface IState {
mainModalOpen: boolean; mainModalOpen: boolean;
mainModalShowed: boolean; mainModalShowed: boolean;
accounts: IAccount[]; accounts: IAccount[];
compileOptions: {
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
strip: boolean
},
deployValues: DeployValue
} }
// let localStorageState: null | string = null; // let localStorageState: null | string = null;
@@ -100,7 +87,6 @@ let initialState: IState = {
logs: [], logs: [],
deployLogs: [], deployLogs: [],
transactionLogs: [], transactionLogs: [],
scriptLogs: [],
editorCtx: undefined, editorCtx: undefined,
gistId: undefined, gistId: undefined,
gistOwner: undefined, gistOwner: undefined,
@@ -116,11 +102,6 @@ let initialState: IState = {
mainModalOpen: false, mainModalOpen: false,
mainModalShowed: false, mainModalShowed: false,
accounts: [], accounts: [],
compileOptions: {
optimizationLevel: '-O2',
strip: true
},
deployValues: {}
}; };
let localStorageAccounts: string | null = null; let localStorageAccounts: string | null = null;
@@ -178,5 +159,3 @@ if (typeof window !== "undefined") {
}); });
} }
export default state export default state
export * from './transactions'

View File

@@ -1,246 +0,0 @@
import { proxy } from 'valtio';
import { deepEqual } from '../utils/object';
import transactionsData from "../content/transactions.json";
import state from '.';
import { showAlert } from "../state/actions/showAlert";
import { parseJSON } from '../utils/json';
export type SelectOption = {
value: string;
label: string;
};
export interface TransactionState {
selectedTransaction: SelectOption | null;
selectedAccount: SelectOption | null;
selectedDestAccount: SelectOption | null;
txIsLoading: boolean;
txIsDisabled: boolean;
txFields: TxFields;
viewType: 'json' | 'ui',
editorSavedValue: null | string,
editorValue?: string,
estimatedFee?: string
}
export type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
export const defaultTransaction: TransactionState = {
selectedTransaction: null,
selectedAccount: null,
selectedDestAccount: null,
txIsLoading: false,
txIsDisabled: false,
txFields: {},
viewType: 'ui',
editorSavedValue: null
};
export const transactionsState = proxy({
transactions: [
{
header: "test1.json",
state: defaultTransaction,
},
],
activeHeader: "test1.json"
});
/**
* Simple transaction state changer
* @param header Unique key and tab name for the transaction tab
* @param partialTx partial transaction state, `undefined` deletes the transaction
*
*/
export const modifyTransaction = (
header: string,
partialTx?: Partial<TransactionState>,
opts: { replaceState?: boolean } = {}
) => {
const tx = transactionsState.transactions.find(tx => tx.header === header);
if (partialTx === undefined) {
transactionsState.transactions = transactionsState.transactions.filter(
tx => tx.header !== header
);
return;
}
if (!tx) {
const state = {
...defaultTransaction,
...partialTx,
}
transactionsState.transactions.push({
header,
state,
});
return state;
}
if (opts.replaceState) {
const repTx: TransactionState = {
...defaultTransaction,
...partialTx,
}
tx.state = repTx
return repTx
}
Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definetly safe!
const s = tx.state as any;
const p = partialTx as any; // ? Make copy
if (!deepEqual(s[k], p[k])) s[k] = p[k];
});
return tx.state
};
// state to tx options
export const prepareTransaction = (data: any) => {
let options = { ...data };
(Object.keys(options)).forEach(field => {
let _value = options[field];
// convert xrp
if (_value && typeof _value === "object" && _value.$type === "xrp") {
if (+_value.$value) {
options[field] = (+_value.$value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (_value && 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 : ""
}`;
console.error(message)
options[field] = _value.$value
}
}
}
// delete unneccesary fields
if (options[field] === undefined) {
delete options[field];
}
});
return options
}
// editor value to state
export const prepareState = (value: string, transactionType?: string) => {
const options = parseJSON(value);
if (!options) {
showAlert("Error!", {
body: "Cannot save editor with malformed transaction."
})
return
};
const { Account, TransactionType, Destination, ...rest } = options;
let tx: Partial<TransactionState> = {};
const txFields = getTxFields(transactionType)
if (Account) {
const acc = state.accounts.find(acc => acc.address === Account);
if (acc) {
tx.selectedAccount = {
label: acc.name,
value: acc.address,
};
} else {
tx.selectedAccount = {
label: Account,
value: Account,
};
}
} else {
tx.selectedAccount = null;
}
if (TransactionType) {
tx.selectedTransaction = {
label: TransactionType,
value: TransactionType,
};
} else {
tx.selectedTransaction = null;
}
if (txFields.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination);
rest.Destination = null
if (dest) {
tx.selectedDestAccount = {
label: dest.name,
value: dest.address,
};
}
else if (Destination) {
tx.selectedDestAccount = {
label: Destination,
value: Destination,
};
}
else {
tx.selectedDestAccount = null
}
}
Object.keys(rest).forEach(field => {
const value = rest[field];
const origValue = txFields[field as keyof TxFields]
const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.$type === 'xrp'
if (isXrp) {
rest[field] = {
$type: "xrp",
$value: +value / 1000000, // ! maybe use bigint?
};
} else if (typeof value === "object") {
rest[field] = {
$type: "json",
$value: value,
};
}
});
tx.txFields = rest;
tx.editorSavedValue = null;
return tx
}
export const getTxFields = (tt?: string) => {
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt
);
if (!txFields) return {}
let _txFields = Object.keys(txFields)
.filter(
key => !["TransactionType", "Account", "Sequence"].includes(key)
)
.reduce<TxFields>(
(tf, key) => (
(tf[key as keyof TxFields] = (txFields as any)[key]), tf
),
{}
);
return _txFields
}
export { transactionsData }

View File

@@ -9,20 +9,16 @@ import {
grass, grass,
slate, slate,
mauve, mauve,
mauveA,
amber, amber,
purple, purple,
green,
grayDark, grayDark,
blueDark, blueDark,
crimsonDark, crimsonDark,
grassDark, grassDark,
slateDark, slateDark,
mauveDark, mauveDark,
mauveDarkA,
amberDark, amberDark,
purpleDark, purpleDark,
greenDark,
red, red,
redDark, redDark,
} from "@radix-ui/colors"; } from "@radix-ui/colors";
@@ -45,10 +41,8 @@ export const {
...grass, ...grass,
...slate, ...slate,
...mauve, ...mauve,
...mauveA,
...amber, ...amber,
...purple, ...purple,
...green,
...red, ...red,
accent: "#9D2DFF", accent: "#9D2DFF",
background: "$gray1", background: "$gray1",
@@ -359,10 +353,8 @@ export const darkTheme = createTheme("dark", {
...grassDark, ...grassDark,
...slateDark, ...slateDark,
...mauveDark, ...mauveDark,
...mauveDarkA,
...amberDark, ...amberDark,
...purpleDark, ...purpleDark,
...greenDark,
...redDark, ...redDark,
deep: "rgb(10, 10, 10)", deep: "rgb(10, 10, 10)",
// backgroundA: transparentize(0.1, grayDark.gray1), // backgroundA: transparentize(0.1, grayDark.gray1),

View File

@@ -1,24 +0,0 @@
import { Spec, parse, Problem } from "comment-parser"
export const getTags = (source?: string): Spec[] => {
if (!source) return []
const blocks = parse(source)
const tags = blocks.reduce(
(acc, block) => acc.concat(block.tags),
[] as Spec[]
);
return tags
}
export const getErrors = (source?: string): Error | undefined => {
if (!source) return undefined
const blocks = parse(source)
const probs = blocks.reduce(
(acc, block) => acc.concat(block.problems),
[] as Problem[]
);
if (!probs.length) return undefined
const errors = probs.map(prob => `[${prob.code}] on line ${prob.line}: ${prob.message}`)
const error = new Error(`The following error(s) occured while parsing JSDOC: \n${errors.join('\n')}`)
return error
}

View File

@@ -1,30 +0,0 @@
import toast from 'react-hot-toast';
import { derive, sign } from "xrpl-accountlib"
import state, { IAccount } from "../state"
const estimateFee = async (tx: Record<string, unknown>, account: IAccount, opts: { silent?: boolean } = {}): Promise<null | { base_fee: string, median_fee: string; minimum_fee: string; open_ledger_fee: string; }> => {
try {
const copyTx = JSON.parse(JSON.stringify(tx))
delete copyTx['SigningPubKey']
if (!copyTx.Fee) {
copyTx.Fee = '1000'
}
const keypair = derive.familySeed(account.secret)
const { signedTransaction } = sign(copyTx, keypair);
const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction })
if (res && res.drops) {
return res.drops;
}
return null
} catch (err) {
if (!opts.silent) {
console.error(err)
toast.error("Cannot estimate fee.") // ? Some better msg
}
return null
}
}
export default estimateFee

View File

@@ -19,13 +19,3 @@ export const extractJSON = (str?: string) => {
firstOpen = str.indexOf('{', firstOpen + 1); firstOpen = str.indexOf('{', firstOpen + 1);
} while (firstOpen != -1); } while (firstOpen != -1);
} }
export const parseJSON = (str?: string | null): any | undefined => {
if (!str) return undefined
try {
const parsed = JSON.parse(str);
return typeof parsed === "object" ? parsed : undefined;
} catch (error) {
return undefined;
}
}

View File

@@ -1,5 +1,6 @@
import { MessageConnection } from "@codingame/monaco-jsonrpc"; import { MessageConnection } from "@codingame/monaco-jsonrpc";
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient"; import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
import Router from "next/router";
import normalizeUrl from "normalize-url"; import normalizeUrl from "normalize-url";
import ReconnectingWebSocket from "reconnecting-websocket"; import ReconnectingWebSocket from "reconnecting-websocket";
@@ -13,8 +14,12 @@ export function createLanguageClient(connection: MessageConnection): MonacoLangu
errorHandler: { errorHandler: {
error: () => ErrorAction.Continue, error: () => ErrorAction.Continue,
closed: () => { closed: () => {
if (Router.pathname.includes('/develop')) {
return CloseAction.Restart
} else {
return CloseAction.DoNotRestart return CloseAction.DoNotRestart
} }
}
}, },
}, },

View File

@@ -1,24 +0,0 @@
export const deepEqual = (object1: any, object2: any) => {
if (!isObject(object1) || !isObject(object2)) return object1 === object2
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
const val1 = object1[key];
const val2 = object2[key];
const areObjects = isObject(val1) && isObject(val2);
if (
areObjects && !deepEqual(val1, val2) ||
!areObjects && val1 !== val2
) {
return false;
}
}
return true;
}
export const isObject = (object: any) => {
return object != null && typeof object === 'object';
}

View File

@@ -1,39 +0,0 @@
export const extractSchemaProps = <O extends object>(obj: O) =>
Object.entries(obj).reduce((prev, [key, val]) => {
const typeOf = <T>(arg: T) =>
arg instanceof Array
? "array"
: arg === null
? "undefined"
: typeof arg;
const value = (typeOf(val) === "object" && '$type' in val && '$value' in val) ? val?.$value : val;
const type = typeOf(value);
let schema: any = {
title: key,
type,
default: value,
}
if (typeOf(value) === 'array') {
const item = value[0] // TODO merge other item schema's into one
if (typeOf(item) !== 'object') {
schema.items = {
type: 'object',
properties: extractSchemaProps(item),
default: item
}
}
// TODO support primitive-value arrays
}
if (typeOf(value) === "object") {
schema.properties = extractSchemaProps(value)
}
return {
...prev,
[key]: schema,
};
}, {} as any);

View File

@@ -3,7 +3,6 @@ import hooksAccountConvBufLen from "./md/hooks-account-conv-buf-len.md";
import hooksAccountConvPure from "./md/hooks-account-conv-pure.md"; import hooksAccountConvPure from "./md/hooks-account-conv-pure.md";
import hooksArrayBufLen from "./md/hooks-array-buf-len.md"; import hooksArrayBufLen from "./md/hooks-array-buf-len.md";
import hooksBurdenPrereq from "./md/hooks-burden-prereq.md"; import hooksBurdenPrereq from "./md/hooks-burden-prereq.md";
import hooksControlStringArg from "./md/hooks-control-string-arg.md";
import hooksDetailBufLen from "./md/hooks-detail-buf-len.md"; import hooksDetailBufLen from "./md/hooks-detail-buf-len.md";
import hooksDetailPrereq from "./md/hooks-detail-prereq.md"; import hooksDetailPrereq from "./md/hooks-detail-prereq.md";
import hooksEmitBufLen from "./md/hooks-emit-buf-len.md"; import hooksEmitBufLen from "./md/hooks-emit-buf-len.md";
@@ -30,18 +29,15 @@ import hooksParamBufLen from "./md/hooks-param-buf-len.md";
import hooksParamSetBufLen from "./md/hooks-param-set-buf-len.md"; import hooksParamSetBufLen from "./md/hooks-param-set-buf-len.md";
import hooksRaddrConvBufLen from "./md/hooks-raddr-conv-buf-len.md"; import hooksRaddrConvBufLen from "./md/hooks-raddr-conv-buf-len.md";
import hooksRaddrConvPure from "./md/hooks-raddr-conv-pure.md"; import hooksRaddrConvPure from "./md/hooks-raddr-conv-pure.md";
import hooksReleaseDefine from "./md/hooks-release-define.md";
import hooksReserveLimit from "./md/hooks-reserve-limit.md"; import hooksReserveLimit from "./md/hooks-reserve-limit.md";
import hooksSlotHashBufLen from "./md/hooks-slot-hash-buf-len.md"; import hooksSlotHashBufLen from "./md/hooks-slot-hash-buf-len.md";
import hooksSlotKeyletBufLen from "./md/hooks-slot-keylet-buf-len.md"; import hooksSlotKeyletBufLen from "./md/hooks-slot-keylet-buf-len.md";
import hooksSlotLimit from "./md/hooks-slot-limit.md"; import hooksSlotLimit from "./md/hooks-slot-limit.md";
import hooksSlotSubLimit from "./md/hooks-slot-sub-limit.md"; import hooksSlotSubLimit from "./md/hooks-slot-sub-limit.md";
import hooksSlotTypeLimit from "./md/hooks-slot-type-limit.md"; import hooksSlotTypeLimit from "./md/hooks-slot-type-limit.md";
import hooksSkipHashBufLen from "./md/hooks-skip-hash-buf-len.md";
import hooksStateBufLen from "./md/hooks-state-buf-len.md"; import hooksStateBufLen from "./md/hooks-state-buf-len.md";
import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md"; import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md";
import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.md"; import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.md";
import hooksTrivialCbak from "./md/hooks-trivial-cbak.md";
import hooksValidateBufLen from "./md/hooks-validate-buf-len.md"; import hooksValidateBufLen from "./md/hooks-validate-buf-len.md";
import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md"; import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md";
@@ -53,7 +49,6 @@ const docs: { [key: string]: string; } = {
"hooks-account-conv-pure": hooksAccountConvPure, "hooks-account-conv-pure": hooksAccountConvPure,
"hooks-array-buf-len": hooksArrayBufLen, "hooks-array-buf-len": hooksArrayBufLen,
"hooks-burden-prereq": hooksBurdenPrereq, "hooks-burden-prereq": hooksBurdenPrereq,
"hooks-control-string-arg": hooksControlStringArg,
"hooks-detail-buf-len": hooksDetailBufLen, "hooks-detail-buf-len": hooksDetailBufLen,
"hooks-detail-prereq": hooksDetailPrereq, "hooks-detail-prereq": hooksDetailPrereq,
"hooks-emit-buf-len": hooksEmitBufLen, "hooks-emit-buf-len": hooksEmitBufLen,
@@ -80,18 +75,15 @@ const docs: { [key: string]: string; } = {
"hooks-param-set-buf-len": hooksParamSetBufLen, "hooks-param-set-buf-len": hooksParamSetBufLen,
"hooks-raddr-conv-buf-len": hooksRaddrConvBufLen, "hooks-raddr-conv-buf-len": hooksRaddrConvBufLen,
"hooks-raddr-conv-pure": hooksRaddrConvPure, "hooks-raddr-conv-pure": hooksRaddrConvPure,
"hooks-release-define": hooksReleaseDefine,
"hooks-reserve-limit": hooksReserveLimit, "hooks-reserve-limit": hooksReserveLimit,
"hooks-slot-hash-buf-len": hooksSlotHashBufLen, "hooks-slot-hash-buf-len": hooksSlotHashBufLen,
"hooks-slot-keylet-buf-len": hooksSlotKeyletBufLen, "hooks-slot-keylet-buf-len": hooksSlotKeyletBufLen,
"hooks-slot-limit": hooksSlotLimit, "hooks-slot-limit": hooksSlotLimit,
"hooks-slot-sub-limit": hooksSlotSubLimit, "hooks-slot-sub-limit": hooksSlotSubLimit,
"hooks-slot-type-limit": hooksSlotTypeLimit, "hooks-slot-type-limit": hooksSlotTypeLimit,
"hooks-skip-hash-buf-len": hooksSkipHashBufLen,
"hooks-state-buf-len": hooksStateBufLen, "hooks-state-buf-len": hooksStateBufLen,
"hooks-transaction-hash-buf-len": hooksTransactionHashBufLen, "hooks-transaction-hash-buf-len": hooksTransactionHashBufLen,
"hooks-transaction-slot-limit": hooksTransactionSlotLimit, "hooks-transaction-slot-limit": hooksTransactionSlotLimit,
"hooks-trivial-cbak": hooksTrivialCbak,
"hooks-validate-buf-len": hooksValidateBufLen, "hooks-validate-buf-len": hooksValidateBufLen,
"hooks-verify-buf-len": hooksVerifyBufLen, "hooks-verify-buf-len": hooksVerifyBufLen,
}; };

View File

@@ -1,5 +0,0 @@
# hooks-control-string-arg
Functions [accept](https://xrpl-hooks.readme.io/v2.0/reference/accept) and [rollback](https://xrpl-hooks.readme.io/v2.0/reference/rollback) take an optional string buffer stored outside the hook as its result message. This is useful for debugging but takes up space.
For a release version, this check warns about constant strings passed to `accept` and `rollback`.

View File

@@ -1,7 +1,7 @@
# hooks-entry-points # hooks-entry-points
A Hook always implements and exports a [hook](https://xrpl-hooks.readme.io/v2.0/reference/hook) function. A Hook always implements and exports exactly two functions: [cbak](https://xrpl-hooks.readme.io/v2.0/reference/cbak) and [hook](https://xrpl-hooks.readme.io/v2.0/reference/hook).
This check shows error on translation units that do not have it. This check shows error on translation units that do not have them.
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks) [Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)

View File

@@ -1,5 +1,5 @@
# hooks-hash-buf-len # hooks-hash-buf-len
Functions [util_sha512h](https://xrpl-hooks.readme.io/v2.0/reference/util_sha512h), [hook_hash](https://xrpl-hooks.readme.io/v2.0/reference/hook_hash), [ledger_last_hash](https://xrpl-hooks.readme.io/v2.0/reference/ledger_last_hash), [etxn_nonce](https://xrpl-hooks.readme.io/v2.0/reference/etxn_nonce) and [ledger_nonce](https://xrpl-hooks.readme.io/v2.0/reference/ledger_nonce) have fixed-size hash output. Functions [util_sha512h](https://xrpl-hooks.readme.io/v2.0/reference/util_sha512h), [hook_hash](https://xrpl-hooks.readme.io/v2.0/reference/hook_hash), [ledger_last_hash](https://xrpl-hooks.readme.io/v2.0/reference/ledger_last_hash) and [nonce](https://xrpl-hooks.readme.io/v2.0/reference/nonce) have fixed-size hash output.
This check warns about too-small size of their output buffer (if it's specified by a constant - variable parameter is ignored). This check warns about too-small size of their output buffer (if it's specified by a constant - variable parameter is ignored).

View File

@@ -1,5 +0,0 @@
# hooks-release-define
Hook users can define a `NDEBUG` macro to disable tracing calls at compile time - but for the definition to be effective, it must be defined before including hook-specific headers.
This check warns when `NDEBUG` is defined too late.

View File

@@ -1,5 +0,0 @@
# hooks-skip-hash-buf-len
Function [hook_skip](https://xrpl-hooks.readme.io/v2.0/reference/hook_skip) has fixed-size canonical hash input.
This check warns about invalid size of its input buffer (if it's specified by a constant - variable parameter is ignored).

View File

@@ -1,7 +0,0 @@
# hooks-trivial-cbak
A Hook may implement and export a [cbak](https://xrpl-hooks.readme.io/v2.0/reference/cbak) function.
But the function is optional, and defining it so that it doesn't do anything besides returning a constant value is unnecessary (except for some debugging scenarios) and just increases the hook size. This check warns about such implementations.
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)

1247
yarn.lock

File diff suppressed because it is too large Load Diff