Compare commits
40 Commits
fix/save-b
...
fix/v2-upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32445dbebf | ||
|
|
1a1d4901aa | ||
|
|
8b646c56dc | ||
|
|
ac38bbc72c | ||
|
|
c2eb57211f | ||
|
|
0e97df3c8e | ||
|
|
5dd0dfdc18 | ||
|
|
ef48bac8f6 | ||
|
|
3a3d984098 | ||
|
|
2300c201f8 | ||
|
|
329dc4a355 | ||
|
|
cd6a5b23d4 | ||
|
|
4dd7cbe2ca | ||
|
|
260de7c838 | ||
|
|
e0ed31f220 | ||
|
|
eba183497f | ||
|
|
4378afa9a1 | ||
|
|
491e10920b | ||
|
|
65bb209713 | ||
|
|
c07e70acc9 | ||
|
|
8fd7f8ecad | ||
|
|
2bb3c646db | ||
|
|
87f10a11b0 | ||
|
|
949fb45ae2 | ||
|
|
ea52f014dd | ||
|
|
77eab8d88d | ||
|
|
4ca8f5f236 | ||
|
|
53f2a71b08 | ||
|
|
866f6257f1 | ||
|
|
814b074cc0 | ||
|
|
386619619b | ||
|
|
d8bf10d0b8 | ||
|
|
d18c893025 | ||
|
|
5e997044ed | ||
|
|
e88720327e | ||
|
|
bf568c3f46 | ||
|
|
1d3bd128f8 | ||
|
|
ab1f45febd | ||
|
|
56a9806b70 | ||
|
|
b3f2d0fb6d |
@@ -304,6 +304,18 @@ const Accounts: FC<AccountProps> = (props) => {
|
||||
if (accountToUpdate) {
|
||||
accountToUpdate.xrp = balance;
|
||||
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) => {
|
||||
@@ -343,7 +355,7 @@ const Accounts: FC<AccountProps> = (props) => {
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [snap.accounts, snap.clientStatus]);
|
||||
}, [snap.accounts.length, snap.clientStatus]);
|
||||
return (
|
||||
<Box
|
||||
as="div"
|
||||
@@ -431,18 +443,23 @@ const Accounts: FC<AccountProps> = (props) => {
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{account.address} (
|
||||
{Dinero({
|
||||
amount: Number(account?.xrp || "0"),
|
||||
precision: 6,
|
||||
})
|
||||
.toUnit()
|
||||
.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "XRP",
|
||||
currencyDisplay: "name",
|
||||
})}
|
||||
)
|
||||
{account.address}{" "}
|
||||
{!account?.error ? (
|
||||
`(${Dinero({
|
||||
amount: Number(account?.xrp || "0"),
|
||||
precision: 6,
|
||||
})
|
||||
.toUnit()
|
||||
.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "XRP",
|
||||
currencyDisplay: "name",
|
||||
})})`
|
||||
) : (
|
||||
<Box css={{ color: "$red11" }}>
|
||||
(Account not found, request funds to activate account)
|
||||
</Box>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
{!props.hideDeployBtn && (
|
||||
|
||||
75
components/AlertDialog/index.tsx
Normal file
75
components/AlertDialog/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { blackA } from "@radix-ui/colors";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import { styled, keyframes } from "../stitches.config";
|
||||
import { styled, keyframes } from "../../stitches.config";
|
||||
|
||||
const overlayShow = keyframes({
|
||||
"0%": { opacity: 0 },
|
||||
@@ -75,7 +75,7 @@ const StyledDescription = styled(AlertDialogPrimitive.Description, {
|
||||
marginBottom: 20,
|
||||
color: "$mauve11",
|
||||
lineHeight: 1.5,
|
||||
fontSize: "$sm",
|
||||
fontSize: "$md",
|
||||
});
|
||||
|
||||
// Exports
|
||||
@@ -40,6 +40,7 @@ const StyledContent = styled(DialogPrimitive.Content, {
|
||||
color: "$mauve12",
|
||||
borderRadius: "$md",
|
||||
position: "relative",
|
||||
mb: "15%",
|
||||
boxShadow:
|
||||
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
|
||||
width: "90vw",
|
||||
|
||||
@@ -50,15 +50,8 @@ import Stack from "./Stack";
|
||||
import { Input, Label } from "./Input";
|
||||
import Text from "./Text";
|
||||
import Tooltip from "./Tooltip";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogCancel,
|
||||
AlertDialogAction,
|
||||
} from "./AlertDialog";
|
||||
import { styled } from "../stitches.config";
|
||||
import { showAlert } from "../state/actions/showAlert";
|
||||
|
||||
const ErrorText = styled(Text, {
|
||||
color: "$error",
|
||||
@@ -68,7 +61,6 @@ const ErrorText = styled(Text, {
|
||||
|
||||
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
||||
const snap = useSnapshot(state);
|
||||
const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
|
||||
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
|
||||
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
|
||||
const [newfileError, setNewfileError] = useState<string | null>(null);
|
||||
@@ -87,13 +79,29 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
||||
setNewfileError(null);
|
||||
}, [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(
|
||||
(filename: string): { error: string | null } => {
|
||||
// check if filename already exists
|
||||
if (!filename) {
|
||||
return { error: "You need to add filename" };
|
||||
}
|
||||
if (snap.files.find((file) => file.name === filename)) {
|
||||
if (snap.files.find(file => file.name === filename)) {
|
||||
return { error: "Filename already exists." };
|
||||
}
|
||||
|
||||
@@ -225,8 +233,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
||||
<Label>Filename</Label>
|
||||
<Input
|
||||
value={filename}
|
||||
onChange={(e) => setFilename(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
onChange={e => setFilename(e.target.value)}
|
||||
onKeyPress={e => {
|
||||
if (e.key === "Enter") {
|
||||
handleConfirm();
|
||||
}
|
||||
@@ -416,7 +424,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
||||
if (snap.gistOwner === session?.user.username) {
|
||||
syncToGist(session);
|
||||
} else {
|
||||
setCreateNewAlertOpen(true);
|
||||
showNewGistAlert();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -466,7 +474,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
||||
<DropdownMenuItem
|
||||
disabled={status !== "authenticated"}
|
||||
onClick={() => {
|
||||
setCreateNewAlertOpen(true);
|
||||
showNewGistAlert();
|
||||
}}
|
||||
>
|
||||
<FilePlus size="16px" /> Create as a new Gist
|
||||
@@ -486,34 +494,6 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
||||
) : null}
|
||||
</Container>
|
||||
</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}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -529,8 +509,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
||||
type="number"
|
||||
min="1"
|
||||
value={editorSettings.tabSize}
|
||||
onChange={(e) =>
|
||||
setEditorSettings((curr) => ({
|
||||
onChange={e =>
|
||||
setEditorSettings(curr => ({
|
||||
...curr,
|
||||
tabSize: Number(e.target.value),
|
||||
}))
|
||||
|
||||
@@ -28,6 +28,28 @@ import {
|
||||
} from "./Dialog";
|
||||
import PanelBox from "./PanelBox";
|
||||
import { templateFileIds } from "../state/constants";
|
||||
import { styled } from "../stitches.config";
|
||||
|
||||
import Starter from "../components/icons/Starter";
|
||||
import Firewall from "../components/icons/Firewall";
|
||||
import Notary from "../components/icons/Notary";
|
||||
import Carbon from "../components/icons/Carbon";
|
||||
import Peggy from "../components/icons/Peggy";
|
||||
|
||||
const ImageWrapper = styled(Flex, {
|
||||
position: "relative",
|
||||
mt: "$2",
|
||||
mb: "$10",
|
||||
svg: {
|
||||
// fill: "red",
|
||||
".angle": {
|
||||
fill: "$text",
|
||||
},
|
||||
":not(.angle)": {
|
||||
stroke: "$text",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Navigation = () => {
|
||||
const router = useRouter();
|
||||
@@ -91,7 +113,7 @@ const Navigation = () => {
|
||||
<Text
|
||||
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
|
||||
>
|
||||
{snap.files.length > 0 ? "Gist: " : "Playground"}
|
||||
{snap.files.length > 0 ? "Gist: " : "Builder"}
|
||||
{snap.files.length > 0 && (
|
||||
<Link
|
||||
href={`https://gist.github.com/${snap.gistOwner || ""}/${
|
||||
@@ -128,19 +150,20 @@ const Navigation = () => {
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
css={{
|
||||
display: "flex",
|
||||
maxWidth: "1080px",
|
||||
width: "80vw",
|
||||
height: "80%",
|
||||
maxHeight: "80%",
|
||||
backgroundColor: "$mauve1 !important",
|
||||
overflowY: "auto",
|
||||
background: "black",
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
css={{
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
height: "auto",
|
||||
height: "100%",
|
||||
"@md": {
|
||||
flexDirection: "row",
|
||||
height: "100%",
|
||||
@@ -151,15 +174,15 @@ const Navigation = () => {
|
||||
css={{
|
||||
borderBottom: "1px solid $colors$mauve5",
|
||||
width: "100%",
|
||||
minWidth: "240px",
|
||||
flexDirection: "column",
|
||||
p: "$7",
|
||||
height: "100%",
|
||||
backgroundColor: "$mauve2",
|
||||
"@md": {
|
||||
width: "30%",
|
||||
maxWidth: "300px",
|
||||
borderBottom: "0px",
|
||||
borderRight: "1px solid $colors$mauve6",
|
||||
borderRight: "1px solid $colors$mauve5",
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -196,9 +219,9 @@ const Navigation = () => {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "$3",
|
||||
color: "$purple10",
|
||||
color: "$purple11",
|
||||
"&:hover": {
|
||||
color: "$purple11",
|
||||
color: "$purple12",
|
||||
},
|
||||
"&:focus": {
|
||||
outline: 0,
|
||||
@@ -217,9 +240,9 @@ const Navigation = () => {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "$3",
|
||||
color: "$purple10",
|
||||
color: "$purple11",
|
||||
"&:hover": {
|
||||
color: "$purple11",
|
||||
color: "$purple12",
|
||||
},
|
||||
"&:focus": {
|
||||
outline: 0,
|
||||
@@ -237,9 +260,9 @@ const Navigation = () => {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "$3",
|
||||
color: "$purple10",
|
||||
color: "$purple11",
|
||||
"&:hover": {
|
||||
color: "$purple11",
|
||||
color: "$purple12",
|
||||
},
|
||||
"&:focus": {
|
||||
outline: 0,
|
||||
@@ -255,67 +278,90 @@ const Navigation = () => {
|
||||
</Flex>
|
||||
</DialogDescription>
|
||||
</Flex>
|
||||
<div>
|
||||
<Flex
|
||||
css={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
|
||||
<Flex
|
||||
css={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
gridTemplateRows: "max-content",
|
||||
flex: 1,
|
||||
p: "$7",
|
||||
pb: "$16",
|
||||
gap: "$3",
|
||||
alignItems: "normal",
|
||||
flexWrap: "wrap",
|
||||
backgroundColor: "$mauve1",
|
||||
"@md": {
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gridTemplateRows: "max-content",
|
||||
flex: 1,
|
||||
p: "$7",
|
||||
gap: "$3",
|
||||
alignItems: "normal",
|
||||
flexWrap: "wrap",
|
||||
backgroundColor: "$mauve1",
|
||||
"@md": {
|
||||
gridTemplateColumns: "1fr 1fr 1fr",
|
||||
gridTemplateRows: "max-content",
|
||||
},
|
||||
}}
|
||||
},
|
||||
"@lg": {
|
||||
gridTemplateColumns: "1fr 1fr 1fr",
|
||||
gridTemplateRows: "max-content",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PanelBox
|
||||
as="a"
|
||||
href={`/develop/${templateFileIds.starter}`}
|
||||
>
|
||||
<PanelBox
|
||||
as="a"
|
||||
href={`/develop/${templateFileIds.starter}`}
|
||||
>
|
||||
<Heading>Starter</Heading>
|
||||
<Text>
|
||||
Just a basic starter with essential imports
|
||||
</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>
|
||||
</Flex>
|
||||
</div>
|
||||
<ImageWrapper>
|
||||
<Starter />
|
||||
</ImageWrapper>
|
||||
<Heading>Starter</Heading>
|
||||
|
||||
<Text>
|
||||
Just a basic starter with essential imports, just
|
||||
accepts any transaction coming through
|
||||
</Text>
|
||||
</PanelBox>
|
||||
|
||||
<PanelBox
|
||||
as="a"
|
||||
href={`/develop/${templateFileIds.firewall}`}
|
||||
css={{ alignItems: "flex-start" }}
|
||||
>
|
||||
<ImageWrapper>
|
||||
<Firewall />
|
||||
</ImageWrapper>
|
||||
<Heading>Firewall</Heading>
|
||||
<Text>
|
||||
This Hook essentially checks a blacklist of accounts
|
||||
</Text>
|
||||
</PanelBox>
|
||||
<PanelBox
|
||||
as="a"
|
||||
href={`/develop/${templateFileIds.notary}`}
|
||||
>
|
||||
<ImageWrapper>
|
||||
<Notary />
|
||||
</ImageWrapper>
|
||||
<Heading>Notary</Heading>
|
||||
<Text>
|
||||
Collecting signatures for multi-sign transactions
|
||||
</Text>
|
||||
</PanelBox>
|
||||
<PanelBox
|
||||
as="a"
|
||||
href={`/develop/${templateFileIds.carbon}`}
|
||||
>
|
||||
<ImageWrapper>
|
||||
<Carbon />
|
||||
</ImageWrapper>
|
||||
<Heading>Carbon</Heading>
|
||||
<Text>Send a percentage of sum to an address</Text>
|
||||
</PanelBox>
|
||||
<PanelBox
|
||||
as="a"
|
||||
href={`/develop/${templateFileIds.peggy}`}
|
||||
>
|
||||
<ImageWrapper>
|
||||
<Peggy />
|
||||
</ImageWrapper>
|
||||
<Heading>Peggy</Heading>
|
||||
<Text>An oracle based stable coin hook</Text>
|
||||
</PanelBox>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<DialogClose asChild>
|
||||
<Box
|
||||
|
||||
@@ -27,6 +27,7 @@ const StyledContent = styled(PopoverPrimitive.Content, {
|
||||
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)": {
|
||||
@@ -43,7 +44,7 @@ const StyledContent = styled(PopoverPrimitive.Content, {
|
||||
".dark &": {
|
||||
backgroundColor: "$mauve5",
|
||||
boxShadow:
|
||||
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
|
||||
"0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -83,12 +84,15 @@ interface IPopover {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const Popover: React.FC<IPopover> = ({
|
||||
const Popover: React.FC<
|
||||
IPopover & React.ComponentProps<typeof PopoverContent>
|
||||
> = ({
|
||||
children,
|
||||
content,
|
||||
open,
|
||||
defaultOpen = false,
|
||||
onOpenChange,
|
||||
...rest
|
||||
}) => (
|
||||
<PopoverRoot
|
||||
open={open}
|
||||
@@ -96,8 +100,8 @@ const Popover: React.FC<IPopover> = ({
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
<PopoverContent sideOffset={5}>
|
||||
{content} <PopoverArrow />
|
||||
<PopoverContent sideOffset={5} {...rest}>
|
||||
{content} <PopoverArrow offset={5} className="arrow" />
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
);
|
||||
|
||||
32
components/Switch.tsx
Normal file
32
components/Switch.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
@@ -45,11 +45,11 @@ const StyledContent = styled(TooltipPrimitive.Content, {
|
||||
},
|
||||
".dark &": {
|
||||
boxShadow:
|
||||
"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",
|
||||
"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",
|
||||
},
|
||||
".light &": {
|
||||
boxShadow:
|
||||
"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",
|
||||
"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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -64,12 +64,15 @@ interface ITooltip {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const Tooltip: React.FC<ITooltip> = ({
|
||||
const Tooltip: React.FC<
|
||||
React.ComponentProps<typeof StyledContent> & ITooltip
|
||||
> = ({
|
||||
children,
|
||||
content,
|
||||
open,
|
||||
defaultOpen = false,
|
||||
onOpenChange,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<TooltipPrimitive.Root
|
||||
@@ -78,8 +81,8 @@ const Tooltip: React.FC<ITooltip> = ({
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
||||
<StyledContent side="bottom" align="center">
|
||||
{content}
|
||||
<StyledContent side="bottom" align="center" {...rest}>
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
<StyledArrow offset={5} width={11} height={5} />
|
||||
</StyledContent>
|
||||
</TooltipPrimitive.Root>
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
import { Play } from "phosphor-react";
|
||||
import { FC, useCallback, useEffect } from "react";
|
||||
import { useSnapshot } from "valtio";
|
||||
import transactionsData from "../content/transactions.json";
|
||||
import state, { modifyTransaction } from "../state";
|
||||
import { sendTransaction } from "../state/actions";
|
||||
import Box from "./Box";
|
||||
import Button from "./Button";
|
||||
import Container from "./Container";
|
||||
import { streamState } from "./DebugStream";
|
||||
import Flex from "./Flex";
|
||||
import Input from "./Input";
|
||||
import Text from "./Text";
|
||||
import Select from "./Select";
|
||||
|
||||
type TxFields = Omit<
|
||||
typeof transactionsData[0],
|
||||
"Account" | "Sequence" | "TransactionType"
|
||||
>;
|
||||
|
||||
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
|
||||
|
||||
type SelectOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export interface TransactionState {
|
||||
selectedTransaction: SelectOption | null;
|
||||
selectedAccount: SelectOption | null;
|
||||
selectedDestAccount: SelectOption | null;
|
||||
txIsLoading: boolean;
|
||||
txIsDisabled: boolean;
|
||||
txFields: TxFields;
|
||||
}
|
||||
|
||||
export interface TransactionProps {
|
||||
header: string;
|
||||
state: TransactionState;
|
||||
}
|
||||
|
||||
const Transaction: FC<TransactionProps> = ({
|
||||
header,
|
||||
state: {
|
||||
selectedAccount,
|
||||
selectedDestAccount,
|
||||
selectedTransaction,
|
||||
txFields,
|
||||
txIsDisabled,
|
||||
txIsLoading,
|
||||
},
|
||||
...props
|
||||
}) => {
|
||||
const { accounts } = useSnapshot(state);
|
||||
|
||||
const setState = useCallback(
|
||||
(pTx?: Partial<TransactionState>) => {
|
||||
modifyTransaction(header, pTx);
|
||||
},
|
||||
[header]
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
const transactionType = selectedTransaction?.value;
|
||||
const account = accounts.find(
|
||||
acc => acc.address === selectedAccount?.value
|
||||
);
|
||||
if (!account || !transactionType || txIsLoading) {
|
||||
setState({ txIsDisabled: true });
|
||||
} else {
|
||||
setState({ txIsDisabled: false });
|
||||
}
|
||||
}, [txIsLoading, selectedTransaction, selectedAccount, accounts, setState]);
|
||||
|
||||
useEffect(() => {
|
||||
let _txFields: TxFields | undefined = transactionsData.find(
|
||||
tx => tx.TransactionType === selectedTransaction?.value
|
||||
);
|
||||
if (!_txFields) return setState({ txFields: {} });
|
||||
_txFields = { ..._txFields } as TxFields;
|
||||
|
||||
if (!_txFields.Destination) setState({ selectedDestAccount: null });
|
||||
// @ts-ignore
|
||||
delete _txFields.TransactionType;
|
||||
// @ts-ignore
|
||||
delete _txFields.Account;
|
||||
// @ts-ignore
|
||||
delete _txFields.Sequence;
|
||||
setState({ txFields: _txFields });
|
||||
}, [setState, selectedTransaction]);
|
||||
|
||||
const submitTest = useCallback(async () => {
|
||||
const account = accounts.find(
|
||||
acc => acc.address === selectedAccount?.value
|
||||
);
|
||||
const TransactionType = selectedTransaction?.value;
|
||||
if (!account || !TransactionType || txIsDisabled) return;
|
||||
|
||||
setState({ txIsLoading: 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 });
|
||||
}
|
||||
}
|
||||
setState({ txIsLoading: false });
|
||||
}, [
|
||||
header,
|
||||
setState,
|
||||
selectedAccount?.value,
|
||||
selectedDestAccount?.value,
|
||||
selectedTransaction?.value,
|
||||
accounts,
|
||||
txFields,
|
||||
txIsDisabled,
|
||||
]);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setState({});
|
||||
}, [setState]);
|
||||
|
||||
const handleSetAccount = (acc: SelectOption) => {
|
||||
setState({ selectedAccount: acc });
|
||||
streamState.selectedAccount = acc;
|
||||
};
|
||||
|
||||
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={(tx: any) => setState({ selectedTransaction: tx })}
|
||||
/>
|
||||
</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.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 =>
|
||||
setState({
|
||||
txFields: {
|
||||
...txFields,
|
||||
Amount: { type: "currency", value: e.target.value },
|
||||
},
|
||||
})
|
||||
}
|
||||
css={{ width: "70%", flex: "inherit" }}
|
||||
/>
|
||||
</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 = 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 =>
|
||||
setState({
|
||||
txFields: {
|
||||
...txFields,
|
||||
[field]:
|
||||
typeof _value === "object"
|
||||
? { ..._value, value: e.target.value }
|
||||
: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
css={{ width: "70%", flex: "inherit" }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Container>
|
||||
<Flex
|
||||
row
|
||||
css={{
|
||||
justifyContent: "space-between",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
mb: "$1"
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transaction;
|
||||
184
components/Transaction/index.tsx
Normal file
184
components/Transaction/index.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
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";
|
||||
|
||||
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;
|
||||
if (viewType === "json") {
|
||||
// save the editor state first
|
||||
const pst = prepareState(editorValue || '', txState);
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
|
||||
{viewType === "json" ? (
|
||||
<TxJson
|
||||
value={jsonValue}
|
||||
header={header}
|
||||
state={txState}
|
||||
setState={setState}
|
||||
/>
|
||||
) : (
|
||||
<TxUI state={txState} setState={setState} />
|
||||
)}
|
||||
<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;
|
||||
205
components/Transaction/json.tsx
Normal file
205
components/Transaction/json.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export const TxJson: FC<JsonProps> = ({
|
||||
value = "",
|
||||
state: txState,
|
||||
header,
|
||||
setState,
|
||||
}) => {
|
||||
const { editorSettings, accounts } = useSnapshot(state);
|
||||
const { editorValue = value, selectedTransaction } = txState;
|
||||
const { theme } = useTheme();
|
||||
const [hasUnsaved, setHasUnsaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setState({ editorValue: value });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorValue === value) setHasUnsaved(false);
|
||||
else setHasUnsaved(true);
|
||||
}, [editorValue, value]);
|
||||
|
||||
const saveState = (value: string, txState: TransactionState) => {
|
||||
const tx = prepareState(value, txState);
|
||||
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, txState);
|
||||
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((): any[] => {
|
||||
const tt = selectedTransaction?.value;
|
||||
const txObj = transactionsData.find(td => td.TransactionType === tt);
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: "file:///account-schema.json",
|
||||
schema: {
|
||||
type: "string",
|
||||
title: "Account type",
|
||||
enum: accounts.map(acc => acc.address),
|
||||
},
|
||||
},
|
||||
{
|
||||
...amountSchema,
|
||||
},
|
||||
];
|
||||
}, [accounts, header, selectedTransaction?.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!monaco) return;
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
schemas: getSchemas(),
|
||||
});
|
||||
}, [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()));
|
||||
|
||||
// set json defaults
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
schemas: getSchemas(),
|
||||
});
|
||||
}}
|
||||
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, txState)}>save</Link>{" "}
|
||||
<Link onClick={discardChanges}>discard</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
213
components/Transaction/ui.tsx
Normal file
213
components/Transaction/ui.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { FC } 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,
|
||||
} from "../../state/transactions";
|
||||
import { useSnapshot } from "valtio";
|
||||
import state from "../../state";
|
||||
import { streamState } from "../DebugStream";
|
||||
|
||||
interface UIProps {
|
||||
setState: (pTx?: Partial<TransactionState> | undefined) => void;
|
||||
state: TransactionState;
|
||||
}
|
||||
|
||||
export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
||||
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 resetOptions = (tt: string) => {
|
||||
const txFields: TxFields | undefined = transactionsData.find(
|
||||
tx => tx.TransactionType === tt
|
||||
);
|
||||
|
||||
if (!txFields) return setState({ txFields: {} });
|
||||
|
||||
const _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),
|
||||
{}
|
||||
);
|
||||
|
||||
if (!_txFields.Destination) setState({ selectedDestAccount: null });
|
||||
setState({ txFields: _txFields });
|
||||
};
|
||||
|
||||
const handleSetAccount = (acc: SelectOption) => {
|
||||
setState({ selectedAccount: acc });
|
||||
streamState.selectedAccount = acc;
|
||||
};
|
||||
|
||||
const handleChangeTxType = (tt: SelectOption) => {
|
||||
setState({ selectedTransaction: tt });
|
||||
resetOptions(tt.value);
|
||||
};
|
||||
|
||||
const specialFields = ["TransactionType", "Account", "Destination"];
|
||||
|
||||
const otherFields = Object.keys(txFields).filter(
|
||||
k => !specialFields.includes(k)
|
||||
) as [keyof TxFields];
|
||||
|
||||
return (
|
||||
<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: 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);
|
||||
} else {
|
||||
value = _value.$value.toString();
|
||||
}
|
||||
} else {
|
||||
value = _value?.toString();
|
||||
}
|
||||
|
||||
let isXrp = typeof _value === "object" && _value.$type === "xrp";
|
||||
return (
|
||||
<Flex
|
||||
key={field}
|
||||
row
|
||||
fluid
|
||||
css={{
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
mb: "$3",
|
||||
pr: "1px",
|
||||
}}
|
||||
>
|
||||
<Text muted css={{ mr: "$3" }}>
|
||||
{field + (isXrp ? " (XRP)" : "")}:{" "}
|
||||
</Text>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={e => {
|
||||
setState({
|
||||
txFields: {
|
||||
...txFields,
|
||||
[field]:
|
||||
typeof _value === "object"
|
||||
? { ..._value, $value: e.target.value }
|
||||
: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
css={{ width: "70%", flex: "inherit" }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
40
components/icons/Carbon.tsx
Normal file
40
components/icons/Carbon.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
75
components/icons/Firewall.tsx
Normal file
75
components/icons/Firewall.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
40
components/icons/Notary.tsx
Normal file
40
components/icons/Notary.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
61
components/icons/Peggy.tsx
Normal file
61
components/icons/Peggy.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
40
components/icons/Starter.tsx
Normal file
40
components/icons/Starter.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
@@ -7,7 +7,7 @@ export { default as Text } from "./Text";
|
||||
export { default as Input, Label } from "./Input";
|
||||
export { default as Select } from "./Select";
|
||||
export * from "./Tabs";
|
||||
export * from "./AlertDialog";
|
||||
export * from "./AlertDialog/primitive";
|
||||
export { default as Box } from "./Box";
|
||||
export { default as Button } from "./Button";
|
||||
export { default as Pre } from "./Pre";
|
||||
|
||||
50
content/amount-schema.json
Normal file
50
content/amount-schema.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"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}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@
|
||||
"Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
|
||||
"TransactionType": "CheckCash",
|
||||
"Amount": {
|
||||
"value": "100",
|
||||
"type": "currency"
|
||||
"$value": "100",
|
||||
"$type": "xrp"
|
||||
},
|
||||
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
|
||||
"Fee": "12"
|
||||
@@ -61,8 +61,8 @@
|
||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||
"TransactionType": "EscrowCreate",
|
||||
"Amount": {
|
||||
"value": "100",
|
||||
"type": "currency"
|
||||
"$value": "100",
|
||||
"$type": "xrp"
|
||||
},
|
||||
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
||||
"CancelAfter": 533257958,
|
||||
@@ -99,8 +99,8 @@
|
||||
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
|
||||
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
|
||||
"Amount": {
|
||||
"value": "100",
|
||||
"type": "currency"
|
||||
"$value": "100",
|
||||
"$type": "xrp"
|
||||
},
|
||||
"Flags": 1
|
||||
},
|
||||
@@ -122,8 +122,8 @@
|
||||
"Sequence": 8,
|
||||
"TakerGets": "6000000",
|
||||
"Amount": {
|
||||
"value": "100",
|
||||
"type": "currency"
|
||||
"$value": "100",
|
||||
"$type": "xrp"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -131,8 +131,8 @@
|
||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
||||
"Amount": {
|
||||
"value": "100",
|
||||
"type": "currency"
|
||||
"$value": "100",
|
||||
"$type": "xrp"
|
||||
},
|
||||
"Fee": "12",
|
||||
"Flags": 2147483648,
|
||||
@@ -142,8 +142,8 @@
|
||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||
"TransactionType": "PaymentChannelCreate",
|
||||
"Amount": {
|
||||
"value": "100",
|
||||
"type": "currency"
|
||||
"$value": "100",
|
||||
"$type": "xrp"
|
||||
},
|
||||
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
||||
"SettleDelay": 86400,
|
||||
@@ -157,8 +157,8 @@
|
||||
"TransactionType": "PaymentChannelFund",
|
||||
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
|
||||
"Amount": {
|
||||
"value": "200",
|
||||
"type": "currency"
|
||||
"$value": "200",
|
||||
"$type": "xrp"
|
||||
},
|
||||
"Expiration": 543171558
|
||||
},
|
||||
@@ -176,8 +176,8 @@
|
||||
"Fee": "12",
|
||||
"SignerQuorum": 3,
|
||||
"SignerEntries": {
|
||||
"type": "json",
|
||||
"value": [
|
||||
"$type": "json",
|
||||
"$value": [
|
||||
{
|
||||
"SignerEntry": {
|
||||
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
||||
@@ -213,8 +213,8 @@
|
||||
"Flags": 262144,
|
||||
"LastLedgerSequence": 8007750,
|
||||
"Amount": {
|
||||
"value": "100",
|
||||
"type": "currency"
|
||||
"$value": "100",
|
||||
"$type": "xrp"
|
||||
},
|
||||
"Sequence": 12
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@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",
|
||||
"@stitches/react": "^1.2.6-0",
|
||||
"base64-js": "^1.5.1",
|
||||
|
||||
@@ -16,6 +16,7 @@ import state from "../state";
|
||||
import TimeAgo from "javascript-time-ago";
|
||||
import en from "javascript-time-ago/locale/en.json";
|
||||
import { useSnapshot } from "valtio";
|
||||
import Alert from "../components/AlertDialog";
|
||||
|
||||
TimeAgo.setDefaultLocale(en.locale);
|
||||
TimeAgo.addLocale(en);
|
||||
@@ -60,22 +61,22 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta property="og:url" content={`${origin}${router.asPath}`} />
|
||||
|
||||
<title>XRPL Hooks Editor</title>
|
||||
<title>XRPL Hooks Builder</title>
|
||||
<meta property="og:title" content="XRPL Hooks Editor" />
|
||||
<meta name="twitter:title" content="XRPL Hooks Editor" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@xrpllabs" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
|
||||
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
|
||||
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger.."
|
||||
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
|
||||
/>
|
||||
<meta property="og:image" content={`${origin}${shareImg}`} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
@@ -140,6 +141,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
||||
})(),
|
||||
}}
|
||||
/>
|
||||
<Alert />
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
</IdProvider>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Label } from "@radix-ui/react-label";
|
||||
import { Switch, SwitchThumb } from "../../components/Switch";
|
||||
import type { NextPage } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Gear, Play } from "phosphor-react";
|
||||
@@ -12,6 +13,7 @@ import Popover from "../../components/Popover";
|
||||
import state from "../../state";
|
||||
import { compileCode } from "../../state/actions";
|
||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
||||
import { styled } from "../../stitches.config";
|
||||
|
||||
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
|
||||
ssr: false,
|
||||
@@ -21,55 +23,177 @@ const LogBox = dynamic(() => import("../../components/LogBox"), {
|
||||
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" }}>
|
||||
<Label>Optimization level</Label>
|
||||
<ButtonGroup css={{ mt: "$2", fontFamily: "$monospace" }}>
|
||||
<Button
|
||||
css={{ fontFamily: "$monospace" }}
|
||||
outline={snap.compileOptions !== "-O0"}
|
||||
onClick={() => (state.compileOptions = "-O0")}
|
||||
<Flex css={{ minWidth: 200, flexDirection: "column", gap: "$5" }}>
|
||||
<Box>
|
||||
<Label
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
-O0
|
||||
</Button>
|
||||
<Button
|
||||
css={{ fontFamily: "$monospace" }}
|
||||
outline={snap.compileOptions !== "-O1"}
|
||||
onClick={() => (state.compileOptions = "-O1")}
|
||||
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>
|
||||
<Box css={{ flexDirection: "column" }}>
|
||||
<Label
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
-O1
|
||||
</Button>
|
||||
<Button
|
||||
css={{ fontFamily: "$monospace" }}
|
||||
outline={snap.compileOptions !== "-O2"}
|
||||
onClick={() => (state.compileOptions = "-O2")}
|
||||
Clean WASM (experimental){" "}
|
||||
<Popover
|
||||
css={{
|
||||
maxWidth: "240px",
|
||||
lineHeight: "1.3",
|
||||
a: {
|
||||
color: "$purple11",
|
||||
},
|
||||
".dark &": {
|
||||
backgroundColor: "$black !important",
|
||||
|
||||
".arrow": {
|
||||
fill: "$colors$black",
|
||||
},
|
||||
},
|
||||
}}
|
||||
content="Cleaner removes unwanted compiler-provided exports and functions from a wasm binary to make it (more) suitable for being used as a Hook"
|
||||
>
|
||||
<Flex
|
||||
css={{
|
||||
position: "relative",
|
||||
top: "-1px",
|
||||
mx: "$1",
|
||||
backgroundColor: "$mauve8",
|
||||
borderRadius: "$full",
|
||||
cursor: "pointer",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
?
|
||||
</Flex>
|
||||
</Popover>
|
||||
</Label>
|
||||
<Switch
|
||||
css={{ mt: "$2" }}
|
||||
checked={snap.compileOptions.strip}
|
||||
onCheckedChange={(checked) => {
|
||||
state.compileOptions.strip = checked;
|
||||
}}
|
||||
>
|
||||
-O2
|
||||
</Button>
|
||||
<Button
|
||||
css={{ fontFamily: "$monospace" }}
|
||||
outline={snap.compileOptions !== "-O3"}
|
||||
onClick={() => (state.compileOptions = "-O3")}
|
||||
>
|
||||
-O3
|
||||
</Button>
|
||||
<Button
|
||||
css={{ fontFamily: "$monospace" }}
|
||||
outline={snap.compileOptions !== "-O4"}
|
||||
onClick={() => (state.compileOptions = "-O4")}
|
||||
>
|
||||
-O4
|
||||
</Button>
|
||||
<Button
|
||||
css={{ fontFamily: "$monospace" }}
|
||||
outline={snap.compileOptions !== "-Os"}
|
||||
onClick={() => (state.compileOptions = "-Os")}
|
||||
>
|
||||
-Os
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<SwitchThumb />
|
||||
</Switch>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,10 +35,11 @@ export const compileCode = async (activeId: number) => {
|
||||
body: JSON.stringify({
|
||||
output: "wasm",
|
||||
compress: true,
|
||||
strip: state.compileOptions.strip,
|
||||
files: [
|
||||
{
|
||||
type: "c",
|
||||
options: state.compileOptions || '-O0',
|
||||
options: state.compileOptions.optimizationLevel || '-O0',
|
||||
name: state.files[activeId].name,
|
||||
src: state.files[activeId].content,
|
||||
},
|
||||
|
||||
@@ -6,13 +6,14 @@ import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
|
||||
import { SetHookData } from "../../components/SetHookDialog";
|
||||
import { Link } from "../../components";
|
||||
import { ref } from "valtio";
|
||||
import estimateFee from "../../utils/estimateFee";
|
||||
|
||||
export const sha256 = async (string: string) => {
|
||||
const utf8 = new TextEncoder().encode(string);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray
|
||||
.map(bytes => bytes.toString(16).padStart(2, "0"))
|
||||
.map((bytes) => bytes.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
return hashHex;
|
||||
};
|
||||
@@ -72,12 +73,12 @@ export const deployHook = async (
|
||||
return;
|
||||
}
|
||||
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 filteredHookParameters = HookParameters.filter(
|
||||
hp =>
|
||||
(hp) =>
|
||||
hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
|
||||
)?.map(aa => ({
|
||||
)?.map((aa) => ({
|
||||
HookParameter: {
|
||||
HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
|
||||
HookParameterValue: aa.HookParameter.HookParameterValue || "",
|
||||
@@ -119,14 +120,22 @@ export const deployHook = async (
|
||||
};
|
||||
|
||||
const keypair = derive.familySeed(account.secret);
|
||||
try {
|
||||
// Update tx Fee value with network estimation
|
||||
await estimateFee(tx, keypair);
|
||||
} catch (err) {
|
||||
// use default value what you defined earlier
|
||||
console.log(err);
|
||||
}
|
||||
const { signedTransaction } = sign(tx, keypair);
|
||||
const currentAccount = state.accounts.find(
|
||||
acc => acc.address === account.address
|
||||
(acc) => acc.address === account.address
|
||||
);
|
||||
if (currentAccount) {
|
||||
currentAccount.isLoading = true;
|
||||
}
|
||||
let submitRes;
|
||||
|
||||
try {
|
||||
submitRes = await state.client.send({
|
||||
command: "submit",
|
||||
@@ -183,7 +192,7 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
|
||||
return;
|
||||
}
|
||||
const currentAccount = state.accounts.find(
|
||||
acc => acc.address === account.address
|
||||
(acc) => acc.address === account.address
|
||||
);
|
||||
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
|
||||
return;
|
||||
@@ -205,6 +214,13 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
|
||||
};
|
||||
|
||||
const keypair = derive.familySeed(account.secret);
|
||||
try {
|
||||
// Update tx Fee value with network estimation
|
||||
await estimateFee(tx, keypair);
|
||||
} catch (err) {
|
||||
// use default value what you defined earlier
|
||||
console.log(err);
|
||||
}
|
||||
const { signedTransaction } = sign(tx, keypair);
|
||||
|
||||
if (currentAccount) {
|
||||
|
||||
23
state/actions/showAlert.ts
Normal file
23
state/actions/showAlert.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -35,6 +35,10 @@ export interface IAccount {
|
||||
hooks: string[];
|
||||
isLoading: boolean;
|
||||
version?: string;
|
||||
error?: {
|
||||
message: string;
|
||||
code: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ILog {
|
||||
@@ -74,7 +78,10 @@ export interface IState {
|
||||
mainModalOpen: boolean;
|
||||
mainModalShowed: boolean;
|
||||
accounts: IAccount[];
|
||||
compileOptions: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
|
||||
compileOptions: {
|
||||
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
|
||||
strip: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// let localStorageState: null | string = null;
|
||||
@@ -104,7 +111,10 @@ let initialState: IState = {
|
||||
mainModalOpen: false,
|
||||
mainModalShowed: false,
|
||||
accounts: [],
|
||||
compileOptions: '-O0'
|
||||
compileOptions: {
|
||||
optimizationLevel: '-O0',
|
||||
strip: true
|
||||
}
|
||||
};
|
||||
|
||||
let localStorageAccounts: string | null = null;
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
import { proxy } from 'valtio';
|
||||
import { TransactionState } from '../components/Transaction';
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
export type TxFields = Omit<
|
||||
typeof transactionsData[0],
|
||||
"Account" | "Sequence" | "TransactionType"
|
||||
>;
|
||||
|
||||
export const defaultTransaction: TransactionState = {
|
||||
selectedTransaction: null,
|
||||
@@ -9,6 +35,8 @@ export const defaultTransaction: TransactionState = {
|
||||
txIsLoading: false,
|
||||
txIsDisabled: false,
|
||||
txFields: {},
|
||||
viewType: 'ui',
|
||||
editorSavedValue: null
|
||||
};
|
||||
|
||||
export const transactionsState = proxy({
|
||||
@@ -24,11 +52,13 @@ export const transactionsState = proxy({
|
||||
/**
|
||||
* Simple transaction state changer
|
||||
* @param header Unique key and tab name for the transaction tab
|
||||
* @param partialTx partial transaction state, `{}` resets the state and `undefined` deletes the transaction
|
||||
* @param partialTx partial transaction state, `undefined` deletes the transaction
|
||||
*
|
||||
*/
|
||||
export const modifyTransaction = (
|
||||
header: string,
|
||||
partialTx?: Partial<TransactionState>
|
||||
partialTx?: Partial<TransactionState>,
|
||||
opts: { replaceState?: boolean } = {}
|
||||
) => {
|
||||
const tx = transactionsState.transactions.find(tx => tx.header === header);
|
||||
|
||||
@@ -40,14 +70,24 @@ export const modifyTransaction = (
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
const state = {
|
||||
...defaultTransaction,
|
||||
...partialTx,
|
||||
}
|
||||
transactionsState.transactions.push({
|
||||
header,
|
||||
state: {
|
||||
...defaultTransaction,
|
||||
...partialTx,
|
||||
},
|
||||
state,
|
||||
});
|
||||
return;
|
||||
return state;
|
||||
}
|
||||
|
||||
if (opts.replaceState) {
|
||||
const repTx: TransactionState = {
|
||||
...defaultTransaction,
|
||||
...partialTx,
|
||||
}
|
||||
tx.state = repTx
|
||||
return repTx
|
||||
}
|
||||
|
||||
Object.keys(partialTx).forEach(k => {
|
||||
@@ -56,4 +96,130 @@ export const modifyTransaction = (
|
||||
const p = partialTx as any;
|
||||
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, txState: TransactionState) => {
|
||||
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 } = txState
|
||||
|
||||
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, // TODO maybe use bigint?
|
||||
};
|
||||
} else if (typeof value === "object") {
|
||||
rest[field] = {
|
||||
$type: "json",
|
||||
$value: value,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
tx.txFields = rest;
|
||||
tx.editorSavedValue = null;
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
export { transactionsData }
|
||||
|
||||
@@ -9,16 +9,20 @@ import {
|
||||
grass,
|
||||
slate,
|
||||
mauve,
|
||||
mauveA,
|
||||
amber,
|
||||
purple,
|
||||
green,
|
||||
grayDark,
|
||||
blueDark,
|
||||
crimsonDark,
|
||||
grassDark,
|
||||
slateDark,
|
||||
mauveDark,
|
||||
mauveDarkA,
|
||||
amberDark,
|
||||
purpleDark,
|
||||
greenDark,
|
||||
red,
|
||||
redDark,
|
||||
} from "@radix-ui/colors";
|
||||
@@ -41,8 +45,10 @@ export const {
|
||||
...grass,
|
||||
...slate,
|
||||
...mauve,
|
||||
...mauveA,
|
||||
...amber,
|
||||
...purple,
|
||||
...green,
|
||||
...red,
|
||||
accent: "#9D2DFF",
|
||||
background: "$gray1",
|
||||
@@ -353,8 +359,10 @@ export const darkTheme = createTheme("dark", {
|
||||
...grassDark,
|
||||
...slateDark,
|
||||
...mauveDark,
|
||||
...mauveDarkA,
|
||||
...amberDark,
|
||||
...purpleDark,
|
||||
...greenDark,
|
||||
...redDark,
|
||||
deep: "rgb(10, 10, 10)",
|
||||
// backgroundA: transparentize(0.1, grayDark.gray1),
|
||||
|
||||
20
utils/estimateFee.ts
Normal file
20
utils/estimateFee.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { sign, XRPL_Account } from "xrpl-accountlib"
|
||||
import state from "../state"
|
||||
|
||||
// Mutate tx object with network estimated fee value
|
||||
const estimateFee = async (tx: Record<string, unknown>, keypair: XRPL_Account): Promise<null | { base_fee: string, median_fee: string; minimum_fee: string; open_ledger_fee: string; }> => {
|
||||
const copyTx = JSON.parse(JSON.stringify(tx))
|
||||
delete copyTx['SigningPubKey']
|
||||
const { signedTransaction } = sign(copyTx, keypair);
|
||||
try {
|
||||
const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction })
|
||||
if (res && res.drops) {
|
||||
return tx['Fee'] = res.drops.base_fee;
|
||||
}
|
||||
return null
|
||||
} catch (err) {
|
||||
throw Error('Cannot estimate fee')
|
||||
}
|
||||
}
|
||||
|
||||
export default estimateFee
|
||||
@@ -18,4 +18,14 @@ export const extractJSON = (str?: string) => {
|
||||
} while (firstClose > firstOpen);
|
||||
firstOpen = str.indexOf('{', 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;
|
||||
}
|
||||
}
|
||||
39
utils/schema.ts
Normal file
39
utils/schema.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
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);
|
||||
|
||||
@@ -3,6 +3,7 @@ import hooksAccountConvBufLen from "./md/hooks-account-conv-buf-len.md";
|
||||
import hooksAccountConvPure from "./md/hooks-account-conv-pure.md";
|
||||
import hooksArrayBufLen from "./md/hooks-array-buf-len.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 hooksDetailPrereq from "./md/hooks-detail-prereq.md";
|
||||
import hooksEmitBufLen from "./md/hooks-emit-buf-len.md";
|
||||
@@ -29,12 +30,14 @@ import hooksParamBufLen from "./md/hooks-param-buf-len.md";
|
||||
import hooksParamSetBufLen from "./md/hooks-param-set-buf-len.md";
|
||||
import hooksRaddrConvBufLen from "./md/hooks-raddr-conv-buf-len.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 hooksSlotHashBufLen from "./md/hooks-slot-hash-buf-len.md";
|
||||
import hooksSlotKeyletBufLen from "./md/hooks-slot-keylet-buf-len.md";
|
||||
import hooksSlotLimit from "./md/hooks-slot-limit.md";
|
||||
import hooksSlotSubLimit from "./md/hooks-slot-sub-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 hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md";
|
||||
import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.md";
|
||||
@@ -49,6 +52,7 @@ const docs: { [key: string]: string; } = {
|
||||
"hooks-account-conv-pure": hooksAccountConvPure,
|
||||
"hooks-array-buf-len": hooksArrayBufLen,
|
||||
"hooks-burden-prereq": hooksBurdenPrereq,
|
||||
"hooks-control-string-arg": hooksControlStringArg,
|
||||
"hooks-detail-buf-len": hooksDetailBufLen,
|
||||
"hooks-detail-prereq": hooksDetailPrereq,
|
||||
"hooks-emit-buf-len": hooksEmitBufLen,
|
||||
@@ -75,12 +79,14 @@ const docs: { [key: string]: string; } = {
|
||||
"hooks-param-set-buf-len": hooksParamSetBufLen,
|
||||
"hooks-raddr-conv-buf-len": hooksRaddrConvBufLen,
|
||||
"hooks-raddr-conv-pure": hooksRaddrConvPure,
|
||||
"hooks-release-define": hooksReleaseDefine,
|
||||
"hooks-reserve-limit": hooksReserveLimit,
|
||||
"hooks-slot-hash-buf-len": hooksSlotHashBufLen,
|
||||
"hooks-slot-keylet-buf-len": hooksSlotKeyletBufLen,
|
||||
"hooks-slot-limit": hooksSlotLimit,
|
||||
"hooks-slot-sub-limit": hooksSlotSubLimit,
|
||||
"hooks-slot-type-limit": hooksSlotTypeLimit,
|
||||
"hooks-skip-hash-buf-len": hooksSkipHashBufLen,
|
||||
"hooks-state-buf-len": hooksStateBufLen,
|
||||
"hooks-transaction-hash-buf-len": hooksTransactionHashBufLen,
|
||||
"hooks-transaction-slot-limit": hooksTransactionSlotLimit,
|
||||
|
||||
5
xrpl-hooks-docs/md/hooks-control-string-arg.md
Normal file
5
xrpl-hooks-docs/md/hooks-control-string-arg.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 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`.
|
||||
5
xrpl-hooks-docs/md/hooks-release-define.md
Normal file
5
xrpl-hooks-docs/md/hooks-release-define.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 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.
|
||||
5
xrpl-hooks-docs/md/hooks-skip-hash-buf-len.md
Normal file
5
xrpl-hooks-docs/md/hooks-skip-hash-buf-len.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 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).
|
||||
17
yarn.lock
17
yarn.lock
@@ -674,7 +674,7 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "0.1.0"
|
||||
|
||||
"@radix-ui/react-label@^0.1.5":
|
||||
"@radix-ui/react-label@0.1.5", "@radix-ui/react-label@^0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-0.1.5.tgz"
|
||||
integrity sha512-Au9+n4/DhvjR0IHhvZ1LPdx/OW+3CGDie30ZyCkbSHIuLp4/CV4oPPGBwJ1vY99Jog3zyQhsGww9MXj8O9Aj/A==
|
||||
@@ -794,6 +794,21 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "0.1.0"
|
||||
|
||||
"@radix-ui/react-switch@^0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-0.1.5.tgz#071ffa19a17a47fdc5c5e6f371bd5901c9fef2f4"
|
||||
integrity sha512-ITtslJPK+Yi34iNf7K9LtsPaLD76oRIVzn0E8JpEO5HW8gpRBGb2NNI9mxKtEB30TVqIcdjdL10AmuIfOMwjtg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "0.1.0"
|
||||
"@radix-ui/react-compose-refs" "0.1.0"
|
||||
"@radix-ui/react-context" "0.1.1"
|
||||
"@radix-ui/react-label" "0.1.5"
|
||||
"@radix-ui/react-primitive" "0.1.4"
|
||||
"@radix-ui/react-use-controllable-state" "0.1.0"
|
||||
"@radix-ui/react-use-previous" "0.1.1"
|
||||
"@radix-ui/react-use-size" "0.1.1"
|
||||
|
||||
"@radix-ui/react-tooltip@^0.1.7":
|
||||
version "0.1.7"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-0.1.7.tgz"
|
||||
|
||||
Reference in New Issue
Block a user