Compare commits

...

36 Commits

Author SHA1 Message Date
muzam1l
7f6b989f15 Update tx state accounts on delete account. 2022-08-02 15:23:11 +05:30
muzamil
6ee1a09aaa Merge pull request #259 from XRPLF/feat/deploy-default-fields
Deploy config default fields from source files.
2022-07-29 15:59:01 +05:30
muzam1l
dd2228fb35 Reset deploy form fields when file changes. 2022-07-29 14:33:28 +05:30
muzam1l
ca52a5e064 Enforce required prop in default tags. 2022-07-28 18:12:58 +05:30
muzam1l
df0f8abe62 Add required margin to param field. 2022-07-27 17:31:43 +05:30
muzam1l
a6c4db1951 Invoke options defaults. 2022-07-26 17:05:33 +05:30
muzam1l
1c91003164 Deploy config default fields from source files. 2022-07-25 20:22:58 +05:30
muzamil
66be0efbbd Merge pull request #255 from XRPLF/feat/tab-renames
Implement file renaming.
2022-07-22 16:35:47 +05:30
muzamil
9ab64ec062 Merge pull request #256 from XRPLF/fix/compile-result
Fix incorrect compilation result.
2022-07-22 14:43:55 +05:30
muzamil
e77a5e234f Merge pull request #257 from XRPLF/fix/account-select
Fixes #240.
2022-07-22 14:43:32 +05:30
muzamil
d2f618512a Merge pull request #258 from XRPLF/fix/acc-import-limit
Remove account import limit.
2022-07-22 14:43:01 +05:30
muzam1l
f5063de2c9 Fix matching file error algo for rename/newfile. 2022-07-22 14:40:40 +05:30
muzam1l
1ee8dcb536 Select comp: remove hightlight from selected option 2022-07-21 16:58:07 +05:30
muzam1l
7f6f9c11db Fix dialog z-index for safari. 2022-07-21 16:44:33 +05:30
muzam1l
b2b7059774 Remove account import limit. 2022-07-21 15:31:06 +05:30
muzam1l
41ba096ef9 Account selector change. 2022-07-21 15:26:37 +05:30
muzam1l
8b72086c04 Differentiate between netwrok error and invalid binary error 2022-07-21 15:18:28 +05:30
muzam1l
895b34cc68 Fix compile result message on invalid wasm 2022-07-21 15:03:16 +05:30
muzam1l
b9da659f83 Fix that weird anonymous zero in the ui. 2022-07-21 14:14:40 +05:30
muzamil
3897f2d823 Merge pull request #246 from XRPLF/feat/tabs-comp
Tabs and Context menu components.
2022-07-20 17:46:04 +05:30
muzam1l
6a3ff3e1d7 Implement tab renaming. 2022-07-20 16:56:55 +05:30
muzam1l
bf792f1495 Fix dependecy issues. 2022-07-20 14:55:59 +05:30
muzam1l
df3210a663 Add context menu component 2022-07-20 14:12:48 +05:30
muzam1l
bad7730c32 Fix extension requirement error. 2022-07-20 14:05:02 +05:30
muzam1l
adb6a78549 update active compiled file based on active editor file. 2022-07-20 14:05:02 +05:30
muzam1l
8cc27f20c3 Migrate File navigation to tabs component. 2022-07-20 14:05:02 +05:30
muzamil
8e49a3f5f1 Merge pull request #253 from XRPLF/fix/json-tx
Fix json mode.
2022-07-20 13:59:42 +05:30
muzam1l
3179757469 Fix TxFields type. 2022-07-19 17:46:13 +05:30
muzam1l
554cfb3db9 Add fee field to all transactions. 2022-07-19 17:45:41 +05:30
muzam1l
637a066f69 Fix tx reset state button 2022-07-19 17:36:09 +05:30
muzam1l
c9a852e9be Add destination field to NFTokenCreateOffer 2022-07-19 16:05:10 +05:30
muzam1l
307a5407eb Fix handling of Destination field in transactions. 2022-07-19 16:01:21 +05:30
muzam1l
faa28845c8 Ensure editor value updation on state change 2022-07-18 21:26:05 +05:30
muzam1l
168d11d48e Remove editorSavedValue from tx state 2022-07-18 19:10:33 +05:30
muzam1l
60f2bb558c Cancel button on json discard dialog. 2022-07-18 17:10:13 +05:30
muzamil
fdf33b9f45 Merge pull request #251 from XRPLF/fix/binary-codec
Fix binary codec (#250)
2022-07-15 16:17:43 +05:30
29 changed files with 1038 additions and 612 deletions

View File

@@ -32,6 +32,7 @@ import { SetHookDialog } from "./SetHookDialog";
import { addFunds } from "../state/actions/addFaucetAccount"; import { addFunds } from "../state/actions/addFaucetAccount";
import { deleteHook } from "../state/actions/deployHook"; import { deleteHook } from "../state/actions/deployHook";
import { capitalize } from "../utils/helpers"; import { capitalize } from "../utils/helpers";
import { deleteAccount } from '../state/actions/deleteAccount';
export const AccountDialog = ({ export const AccountDialog = ({
activeAccountAddress, activeAccountAddress,
@@ -99,10 +100,7 @@ export const AccountDialog = ({
css={{ ml: "auto", mr: "$9" }} css={{ ml: "auto", mr: "$9" }}
tabIndex={-1} tabIndex={-1}
onClick={() => { onClick={() => {
const index = state.accounts.findIndex( deleteAccount(activeAccount?.address);
acc => acc.address === activeAccount?.address
);
state.accounts.splice(index, 1);
}} }}
> >
Delete Account <Trash size="15px" /> Delete Account <Trash size="15px" />

View File

@@ -0,0 +1,135 @@
import { CaretRight, Check, Circle } from "phosphor-react";
import { FC, Fragment, ReactNode } from "react";
import { Flex, Text } from "../";
import {
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuItemIndicator,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuRoot,
ContextMenuSeparator,
ContextMenuTrigger,
ContextMenuTriggerItem,
} from "./primitive";
export type TextOption = {
type: "text";
label: ReactNode;
onSelect?: () => any;
children?: ContentMenuOption[];
};
export type SeparatorOption = { type: "separator" };
export type CheckboxOption = {
type: "checkbox";
label: ReactNode;
checked?: boolean;
onCheckedChange?: (isChecked: boolean) => any;
};
export type RadioOption<T extends string = string> = {
type: "radio";
label: ReactNode;
onValueChange?: (value: string) => any;
value: T;
options?: { value: T; label?: ReactNode }[];
};
type WithCommons = { key: string; disabled?: boolean };
export type ContentMenuOption = (
| TextOption
| SeparatorOption
| CheckboxOption
| RadioOption
) &
WithCommons;
export interface IContextMenu {
options?: ContentMenuOption[];
isNested?: boolean;
}
export const ContextMenu: FC<IContextMenu> = ({
children,
options,
isNested,
}) => {
return (
<ContextMenuRoot>
{isNested ? (
<ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
) : (
<ContextMenuTrigger>{children}</ContextMenuTrigger>
)}
{options && !!options.length && (
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
{options.map(({ key, ...option }) => {
if (option.type === "text") {
const { children, label, onSelect } = option;
if (children)
return (
<ContextMenu isNested key={key} options={children}>
<Flex fluid row justify="space-between" align="center">
<Text>{label}</Text>
<CaretRight />
</Flex>
</ContextMenu>
);
return (
<ContextMenuItem key={key} onSelect={onSelect}>
{label}
</ContextMenuItem>
);
}
if (option.type === "checkbox") {
const { label, checked, onCheckedChange } = option;
return (
<ContextMenuCheckboxItem
key={key}
checked={checked}
onCheckedChange={onCheckedChange}
>
<Flex row align="center">
<ContextMenuItemIndicator>
<Check />
</ContextMenuItemIndicator>
<Text css={{ ml: checked ? "$4" : undefined }}>
{label}
</Text>
</Flex>
</ContextMenuCheckboxItem>
);
}
if (option.type === "radio") {
const { label, options, onValueChange, value } = option;
return (
<Fragment key={key}>
<ContextMenuLabel>{label}</ContextMenuLabel>
<ContextMenuRadioGroup
value={value}
onValueChange={onValueChange}
>
{options?.map(({ value: v, label }) => {
return (
<ContextMenuRadioItem key={v} value={v}>
<ContextMenuItemIndicator>
<Circle weight="fill" />
</ContextMenuItemIndicator>
<Text css={{ ml: "$4" }}>{label}</Text>
</ContextMenuRadioItem>
);
})}
</ContextMenuRadioGroup>
</Fragment>
);
}
return <ContextMenuSeparator key={key} />;
})}
</ContextMenuContent>
)}
</ContextMenuRoot>
);
};
export default ContextMenu;

View File

@@ -0,0 +1,107 @@
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { styled } from "../../stitches.config";
import {
slideDownAndFade,
slideLeftAndFade,
slideRightAndFade,
slideUpAndFade,
} from "../../styles/keyframes";
const StyledContent = styled(ContextMenuPrimitive.Content, {
minWidth: 140,
backgroundColor: "$backgroundOverlay",
borderRadius: 6,
overflow: "hidden",
padding: "5px",
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 &": {
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
},
});
const itemStyles = {
all: "unset",
fontSize: 13,
lineHeight: 1,
color: "$text",
borderRadius: 3,
display: "flex",
alignItems: "center",
height: 28,
padding: "0 7px",
position: "relative",
paddingLeft: 10,
userSelect: "none",
"&[data-disabled]": {
color: "$textMuted",
pointerEvents: "none",
},
"&:focus": {
backgroundColor: "$purple9",
color: "$white",
},
};
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles });
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
...itemStyles,
});
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
...itemStyles,
});
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
'&[data-state="open"]': {
backgroundColor: "$purple9",
color: "$purple9",
},
...itemStyles,
});
const StyledLabel = styled(ContextMenuPrimitive.Label, {
paddingLeft: 10,
fontSize: 12,
lineHeight: "25px",
color: "$text",
});
const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
height: 1,
backgroundColor: "$backgroundAlt",
margin: 5,
});
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, {
position: "absolute",
left: 0,
width: 25,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
});
export const ContextMenuRoot = ContextMenuPrimitive.Root;
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
export const ContextMenuContent = StyledContent;
export const ContextMenuItem = StyledItem;
export const ContextMenuCheckboxItem = StyledCheckboxItem;
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
export const ContextMenuRadioItem = StyledRadioItem;
export const ContextMenuItemIndicator = StyledItemIndicator;
export const ContextMenuTriggerItem = StyledTriggerItem;
export const ContextMenuLabel = StyledLabel;
export const ContextMenuSeparator = StyledSeparator;

View File

@@ -13,7 +13,7 @@ import state from "../state";
import wat from "../utils/wat-highlight"; import wat from "../utils/wat-highlight";
import EditorNavigation from "./EditorNavigation"; import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex } from "."; import { Button, Text, Link, Flex, Tabs, Tab } from ".";
import Monaco from "./Monaco"; import Monaco from "./Monaco";
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]; const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
@@ -25,9 +25,20 @@ const DeployEditor = () => {
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
const activeFile = snap.files[snap.active]?.compiledContent const compiledFiles = snap.files.filter(file => file.compiledContent);
? snap.files[snap.active] const activeFile = compiledFiles[snap.activeWat];
: snap.files.filter(file => file.compiledContent)[0];
const renderNav = () => (
<Tabs
activeIndex={snap.activeWat}
onChangeActive={idx => (state.activeWat = idx)}
>
{compiledFiles.map((file, index) => {
return <Tab key={file.name} header={`${file.name}.wat`} />;
})}
</Tabs>
);
const compiledSize = activeFile?.compiledContent?.byteLength || 0; const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color = const color =
compiledSize > FILESIZE_BREAKPOINTS[1] compiledSize > FILESIZE_BREAKPOINTS[1]
@@ -38,7 +49,7 @@ const DeployEditor = () => {
const isContentChanged = const isContentChanged =
activeFile && activeFile.compiledValueSnapshot !== activeFile.content; activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
// const hasDeployErros = activeFile && activeFile.containsErrors; // const hasDeployErrors = activeFile && activeFile.containsErrors;
const CompiledStatView = activeFile && ( const CompiledStatView = activeFile && (
<Flex <Flex
@@ -99,6 +110,7 @@ const DeployEditor = () => {
</NextLink> </NextLink>
</Text> </Text>
); );
const isContent = const isContent =
snap.files?.filter(file => file.compiledWatContent).length > 0 && snap.files?.filter(file => file.compiledWatContent).length > 0 &&
router.isReady; router.isReady;
@@ -113,7 +125,7 @@ const DeployEditor = () => {
width: "100%", width: "100%",
}} }}
> >
<EditorNavigation showWat /> <EditorNavigation renderNav={renderNav} />
<Container <Container
css={{ css={{
display: "flex", display: "flex",

View File

@@ -15,7 +15,7 @@ const contentShow = keyframes({
"100%": { opacity: 1 }, "100%": { opacity: 1 },
}); });
const StyledOverlay = styled(DialogPrimitive.Overlay, { const StyledOverlay = styled(DialogPrimitive.Overlay, {
zIndex: 9999, zIndex: 10000,
backgroundColor: blackA.blackA9, backgroundColor: blackA.blackA9,
position: "fixed", position: "fixed",
inset: 0, inset: 0,

View File

@@ -1,27 +1,7 @@
import { keyframes } from "@stitches/react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import { slideDownAndFade, slideLeftAndFade, slideRightAndFade, slideUpAndFade } from '../styles/keyframes';
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(DropdownMenuPrimitive.Content, { const StyledContent = styled(DropdownMenuPrimitive.Content, {
minWidth: 220, minWidth: 220,

View File

@@ -1,6 +1,10 @@
import React, { useState, useEffect, useCallback, useRef } from "react"; import React, {
useState,
useEffect,
useRef,
ReactNode,
} from "react";
import { import {
Plus,
Share, Share,
DownloadSimple, DownloadSimple,
Gear, Gear,
@@ -28,7 +32,6 @@ import { useSnapshot } from "valtio";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { import {
createNewFile,
syncToGist, syncToGist,
updateEditorSettings, updateEditorSettings,
downloadAsZip, downloadAsZip,
@@ -48,36 +51,23 @@ import {
import Flex from "./Flex"; import Flex from "./Flex";
import Stack from "./Stack"; import Stack from "./Stack";
import { Input, Label } from "./Input"; import { Input, Label } from "./Input";
import Text from "./Text";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import { styled } from "../stitches.config";
import { showAlert } from "../state/actions/showAlert"; import { showAlert } from "../state/actions/showAlert";
const ErrorText = styled(Text, {
color: "$error",
mt: "$1",
display: "block",
});
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => { const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false); const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null);
const [filename, setFilename] = useState("");
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const [popup, setPopUp] = useState(false); const [popup, setPopUp] = useState(false);
const [editorSettings, setEditorSettings] = useState(snap.editorSettings); const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
useEffect(() => { useEffect(() => {
if (session && session.user && popup) { if (session && session.user && popup) {
setPopUp(false); setPopUp(false);
} }
}, [session, popup]); }, [session, popup]);
// when filename changes, reset error
useEffect(() => {
setNewfileError(null);
}, [filename, setNewfileError]);
const showNewGistAlert = () => { const showNewGistAlert = () => {
showAlert("Are you sure?", { showAlert("Are you sure?", {
@@ -95,46 +85,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
}); });
}; };
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)) {
return { error: "Filename already exists." };
}
if (!filename.includes(".") || filename[filename.length - 1] === ".") {
return { error: "Filename should include file extension" };
}
// check for illegal characters
const ALPHA_NUMERICAL_REGEX = /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g;
if (!filename.match(ALPHA_NUMERICAL_REGEX)) {
return {
error: `Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-" and it needs to have file extension (e.g. ".c")`,
};
}
return { error: null };
},
[snap.files]
);
const handleConfirm = useCallback(() => {
// add default extension in case omitted
const chk = validateFilename(filename);
if (chk && chk.error) {
setNewfileError(`Error: ${chk.error}`);
return;
}
setIsNewfileDialogOpen(false);
createNewFile(filename);
setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const files = snap.files;
return ( return (
<Flex css={{ flexShrink: 0, gap: "$0" }}> <Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex <Flex
@@ -174,131 +126,14 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
scrollbarWidth: "thin", scrollbarWidth: "thin",
}, },
}} }}
onWheelCapture={(e) => { onWheelCapture={e => {
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY; scrollRef.current.scrollLeft += e.deltaY;
} }
}} }}
> >
<Container css={{ flex: 1 }} ref={containerRef}> <Container css={{ flex: 1 }} ref={containerRef}>
<Stack {renderNav?.()}
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
}}
>
{files &&
files.length > 0 &&
files.map((file, index) => {
if (!file.compiledContent && showWat) {
return null;
}
return (
<Button
size="sm"
outline={
showWat ? snap.activeWat !== index : snap.active !== index
}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{file.name}
{showWat && ".wat"}
{!showWat && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active =
index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
);
})}
{!showWat && (
<Dialog
open={isNewfileDialogOpen}
onOpenChange={setIsNewfileDialogOpen}
>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<Label>Filename</Label>
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleConfirm();
}
}}
/>
<ErrorText>{newfileError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleConfirm}>
Create file
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
</Stack>
</Container> </Container>
</Flex> </Flex>
<Flex <Flex
@@ -542,8 +377,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
type="number" type="number"
min="1" min="1"
value={editorSettings.tabSize} value={editorSettings.tabSize}
onChange={(e) => onChange={e =>
setEditorSettings((curr) => ({ setEditorSettings(curr => ({
...curr, ...curr,
tabSize: Number(e.target.value), tabSize: Number(e.target.value),
})) }))

View File

@@ -7,7 +7,7 @@ import { useRouter } from "next/router";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import { saveFile } from "../state/actions"; import { createNewFile, saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants"; import { apiHeaderFiles } from "../state/constants";
import state from "../state"; import state from "../state";
@@ -20,7 +20,9 @@ import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs"; import docs from "../xrpl-hooks-docs/docs";
import Monaco from "./Monaco"; import Monaco from "./Monaco";
import { saveAllFiles } from '../state/actions/saveFile'; import { saveAllFiles } from "../state/actions/saveFile";
import { Tab, Tabs } from "./Tabs";
import { renameFile } from "../state/actions/createNewFile";
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path; const currPath = editor.getModel()?.uri.path;
@@ -119,6 +121,27 @@ const HooksEditor = () => {
}, []); }, []);
const file = snap.files[snap.active]; const file = snap.files[snap.active];
const renderNav = () => (
<Tabs
label="File"
activeIndex={snap.active}
onChangeActive={idx => (state.active = idx)}
extensionRequired
onCreateNewTab={createNewFile}
onCloseTab={idx => state.files.splice(idx, 1)}
onRenameTab={(idx, nwName, oldName = "") => renameFile(oldName, nwName)}
headerExtraValidation={{
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
error:
'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"',
}}
>
{snap.files.map((file, index) => {
return <Tab key={file.name} header={file.name} />;
})}
</Tabs>
);
return ( return (
<Box <Box
css={{ css={{
@@ -131,7 +154,7 @@ const HooksEditor = () => {
width: "100%", width: "100%",
}} }}
> >
<EditorNavigation /> <EditorNavigation renderNav={renderNav} />
{snap.files.length > 0 && router.isReady ? ( {snap.files.length > 0 && router.isReady ? (
<Monaco <Monaco
keepCurrentModel keepCurrentModel

View File

@@ -162,7 +162,7 @@ export const Log: FC<ILog> = ({
const enrichAccounts = useCallback( const enrichAccounts = useCallback(
(str?: string): ReactNode => { (str?: string): ReactNode => {
if (!str || !accounts.length) return null; if (!str || !accounts.length) return str;
const pattern = `(${accounts.map(acc => acc.address).join("|")})`; const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
const res = regexifyString({ const res = regexifyString({

View File

@@ -91,7 +91,7 @@ const Select = forwardRef<any, Props>((props, ref) => {
...provided, ...provided,
color: colors.searchText, color: colors.searchText,
backgroundColor: backgroundColor:
state.isSelected || state.isFocused state.isFocused
? colors.activeLight ? colors.activeLight
: colors.dropDownBg, : colors.dropDownBg,
":hover": { ":hover": {

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { Plus, Trash, X } from "phosphor-react"; import { Plus, Trash, X } from "phosphor-react";
import Button from "./Button"; import { Button, Box, Text } from ".";
import Box from "./Box";
import { Stack, Flex, Select } from "."; import { Stack, Flex, Select } from ".";
import { import {
Dialog, Dialog,
@@ -19,48 +18,30 @@ import {
useForm, useForm,
} from "react-hook-form"; } from "react-hook-form";
import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions"; import { deployHook } from "../state/actions";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state, { SelectOption } from "../state"; import state, { IFile, SelectOption } from "../state";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook"; import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
import estimateFee from "../utils/estimateFee"; import estimateFee from "../utils/estimateFee";
import {
const transactionOptions = Object.keys(tts).map(key => ({ getParameters,
label: key, getInvokeOptions,
value: key as keyof TTS, transactionOptions,
})); SetHookData,
} from "../utils/setHook";
export type SetHookData = { import { capitalize } from "../utils/helpers";
Invoke: {
value: keyof TTS;
label: string;
}[];
Fee: string;
HookNamespace: string;
HookParameters: {
HookParameter: {
HookParameterName: string;
HookParameterValue: string;
};
}[];
// HookGrants: {
// HookGrant: {
// Authorize: string;
// HookHash: string;
// };
// }[];
};
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo( export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => { ({ accountAddress }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const activeFile = snap.files[snap.active]?.compiledContent
? snap.files[snap.active] const [estimateLoading, setEstimateLoading] = useState(false);
: snap.files.filter(file => file.compiledContent)[0];
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false); const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const compiledFiles = snap.files.filter(file => file.compiledContent);
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({ const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
@@ -72,6 +53,26 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const account = snap.accounts.find( const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value acc => acc.address === selectedAccount?.value
); );
const getHookNamespace = useCallback(
() =>
(activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
activeFile?.name.split(".")[0] ||
"",
[activeFile, snap.deployValues]
);
const getDefaultValues = useCallback((): Partial<SetHookData> => {
const content = activeFile?.compiledValueSnapshot;
return (
(activeFile && snap.deployValues[activeFile.name]) || {
HookNamespace: getHookNamespace(),
Invoke: getInvokeOptions(content),
HookParameters: getParameters(content),
}
);
}, [activeFile, getHookNamespace, snap.deployValues]);
const { const {
register, register,
handleSubmit, handleSubmit,
@@ -79,38 +80,26 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
watch, watch,
setValue, setValue,
getValues, getValues,
reset,
formState: { errors }, formState: { errors },
} = useForm<SetHookData>({ } = useForm<SetHookData>({
defaultValues: snap.deployValues?.[activeFile?.name] defaultValues: getDefaultValues(),
? snap.deployValues[activeFile?.name]
: {
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"); const watchedFee = watch("Fee");
// Update value if activeWat changes // Reset form if activeFile changes
useEffect(() => { useEffect(() => {
const defaultValue = snap.deployValues?.[activeFile?.name] if (!activeFile) return;
? snap.deployValues?.[activeFile?.name].HookNamespace const defaultValues = getDefaultValues();
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "";
setValue("HookNamespace", defaultValue); reset(defaultValues);
setFormInitialized(true); }, [activeFile, getDefaultValues, reset]);
}, [
snap.activeWat,
snap.files,
setValue,
activeFile?.name,
snap.deployValues,
]);
useEffect(() => { useEffect(() => {
if ( if (
watchedFee && watchedFee &&
@@ -128,54 +117,51 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
// name: "HookGrants", // unique name for your Field Array // name: "HookGrants", // unique name for your Field Array
// }); // });
const [hashedNamespace, setHashedNamespace] = useState(""); const [hashedNamespace, setHashedNamespace] = useState("");
const namespace = watch(
"HookNamespace", const namespace = watch("HookNamespace", getHookNamespace());
snap.deployValues?.[activeFile?.name]
? 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);
setHashedNamespace(hashedVal.toUpperCase()); setHashedNamespace(hashedVal.toUpperCase());
}, [namespace]); }, [namespace]);
useEffect(() => { useEffect(() => {
calculateHashedValue(); calculateHashedValue();
}, [namespace, calculateHashedValue]); }, [namespace, calculateHashedValue]);
// Calcucate initial fee estimate when modal opens const calculateFee = useCallback(async () => {
useEffect(() => { if (!account) return;
if (formInitialized && account) {
(async () => { const formValues = getValues();
const formValues = getValues(); const tx = await prepareDeployHookTx(account, formValues);
const tx = await prepareDeployHookTx(account, formValues); if (!tx) {
if (!tx) { return;
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 const res = await estimateFee(tx, account);
}, [formInitialized]); if (res && res.base_fee) {
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
}
}, [account, getValues, setValue]);
const tooLargeFile = () => { const tooLargeFile = () => {
const activeFile = snap.files[snap.active].compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
return Boolean( return Boolean(
activeFile?.compiledContent?.byteLength && activeFile?.compiledContent?.byteLength &&
activeFile?.compiledContent?.byteLength >= 64000 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 (!account) return;
if (currAccount) currAccount.isLoading = true; if (currAccount) currAccount.isLoading = true;
data.HookParameters.forEach(param => {
delete param.$metaData;
return param;
});
const res = await deployHook(account, data); const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false; if (currAccount) currAccount.isLoading = false;
@@ -185,8 +171,14 @@ 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})`);
}; };
const onOpenChange = useCallback((open: boolean) => {
setIsSetHookDialogOpen(open);
if (open) calculateFee();
}, [calculateFee]);
return ( return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}> <Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
ghost ghost
@@ -194,10 +186,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
uppercase uppercase
variant={"secondary"} variant={"secondary"}
disabled={ disabled={
!account || !account || account.isLoading || !activeFile || tooLargeFile()
account.isLoading ||
!snap.files.filter(file => file.compiledWatContent).length ||
tooLargeFile()
} }
> >
Set Hook Set Hook
@@ -213,7 +202,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Select <Select
instanceId="deploy-account" instanceId="deploy-account"
placeholder="Select account" placeholder="Select account"
hideSelectedOptions
options={accountOptions} options={accountOptions}
value={selectedAccount} value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)} onChange={(acc: any) => setSelectedAccount(acc)}
@@ -259,22 +247,39 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Stack> <Stack>
{fields.map((field, index) => ( {fields.map((field, index) => (
<Stack key={field.id}> <Stack key={field.id}>
<Input <Flex column>
// important to include key with field's id <Flex row>
placeholder="Parameter name" <Input
{...register( // important to include key with field's id
`HookParameters.${index}.HookParameter.HookParameterName` placeholder="Parameter name"
readOnly={field.$metaData?.required}
{...register(
`HookParameters.${index}.HookParameter.HookParameterName`
)}
/>
<Input
css={{ mx: "$2" }}
placeholder="Value (hex-quoted)"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`,
{ required: field.$metaData?.required }
)}
/>
<Button
onClick={() => remove(index)}
variant="destroy"
>
<Trash weight="regular" size="16px" />
</Button>
</Flex>
{errors.HookParameters?.[index]?.HookParameter
?.HookParameterValue?.type === "required" && (
<Text error>This field is required</Text>
)} )}
/> <Label css={{ fontSize: "$sm", mt: "$1" }}>
<Input {capitalize(field.$metaData?.description)}
placeholder="Value (hex-quoted)" </Label>
{...register( </Flex>
`HookParameters.${index}.HookParameter.HookParameterValue`
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
</Stack> </Stack>
))} ))}
<Button <Button

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, Label, Pre, Stack, Text } from ".";
import { import {
Dialog, Dialog,
DialogTrigger, DialogTrigger,
@@ -17,6 +17,8 @@ import {
} from "./Dialog"; } from "./Dialog";
import { Plus, X } from "phosphor-react"; import { Plus, X } from "phosphor-react";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import { capitalize } from "../utils/helpers";
import ContextMenu, { ContentMenuOption } from "./ContextMenu";
const ErrorText = styled(Text, { const ErrorText = styled(Text, {
color: "$error", color: "$error",
@@ -24,12 +26,14 @@ const ErrorText = styled(Text, {
display: "block", display: "block",
}); });
type Nullable<T> = T | null | undefined | false;
interface TabProps { interface TabProps {
header?: string; header: string;
children: ReactNode; children?: ReactNode;
} }
// TODO customise messages shown // TODO customize messages shown
interface Props { interface Props {
label?: string; label?: string;
activeIndex?: number; activeIndex?: number;
@@ -38,9 +42,14 @@ interface Props {
children: ReactElement<TabProps>[]; children: ReactElement<TabProps>[];
keepAllAlive?: boolean; keepAllAlive?: boolean;
defaultExtension?: string; defaultExtension?: string;
appendDefaultExtension?: boolean; extensionRequired?: boolean;
allowedExtensions?: string[]; allowedExtensions?: string[];
headerExtraValidation?: {
regex: string | RegExp;
error: string;
};
onCreateNewTab?: (name: string) => any; onCreateNewTab?: (name: string) => any;
onRenameTab?: (index: number, nwName: string, oldName?: string) => any;
onCloseTab?: (index: number, header?: string) => any; onCloseTab?: (index: number, header?: string) => any;
onChangeActive?: (index: number, header?: string) => any; onChangeActive?: (index: number, header?: string) => any;
} }
@@ -57,16 +66,19 @@ export const Tabs = ({
onCreateNewTab, onCreateNewTab,
onCloseTab, onCloseTab,
onChangeActive, onChangeActive,
onRenameTab,
headerExtraValidation,
extensionRequired,
defaultExtension = "", defaultExtension = "",
appendDefaultExtension = false,
allowedExtensions, 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 [renamingTab, setRenamingTab] = useState<number | null>(null);
const [tabname, setTabname] = useState(""); const [tabname, setTabname] = useState("");
const [newtabError, setNewtabError] = useState<string | null>(null); const [tabnameError, setTabnameError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
if (activeIndex) setActive(activeIndex); if (activeIndex) setActive(activeIndex);
@@ -82,21 +94,46 @@ export const Tabs = ({
// when filename changes, reset error // when filename changes, reset error
useEffect(() => { useEffect(() => {
setNewtabError(null); setTabnameError(null);
}, [tabname, setNewtabError]); }, [tabname, setTabnameError]);
const validateTabname = useCallback( const validateTabname = useCallback(
(tabname: string): { error: string | null } => { (tabname: string): { error?: string, result?: string } => {
if (tabs.find(tab => tab.header === tabname)) { if (!tabname) {
return { error: "Name already exists." }; return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
}
let ext =
(tabname.includes(".") && tabname.split(".").pop()) || "";
if (!ext && defaultExtension) {
ext = defaultExtension
tabname = `${tabname}.${defaultExtension}`
}
if (tabs.find(tab => tab.header === tabname)) {
return { error: `${capitalize(label)} name already exists.` };
}
if (extensionRequired && !ext) {
return { error: "File extension is required!" };
} }
const ext = tabname.split(".").pop() || "";
if (allowedExtensions && !allowedExtensions.includes(ext)) { if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: "This file extension is not allowed!" }; return { error: "This file extension is not allowed!" };
} }
return { error: null }; if (
headerExtraValidation &&
!tabname.match(headerExtraValidation.regex)
) {
return { error: headerExtraValidation.error };
}
return { result: tabname };
}, },
[allowedExtensions, tabs] [
allowedExtensions,
defaultExtension,
extensionRequired,
headerExtraValidation,
label,
tabs,
]
); );
const handleActiveChange = useCallback( const handleActiveChange = useCallback(
@@ -107,37 +144,41 @@ export const Tabs = ({
[onChangeActive] [onChangeActive]
); );
const handleCreateTab = useCallback(() => { const handleRenameTab = useCallback(() => {
// add default extension in case omitted if (renamingTab === null) return;
let _tabname = tabname.includes(".")
? tabname
: `${tabname}.${defaultExtension}`;
if (appendDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
}
const chk = validateTabname(_tabname); const res = validateTabname(tabname);
if (chk.error) { if (res.error) {
setNewtabError(`Error: ${chk.error}`); setTabnameError(`Error: ${res.error}`);
return; return;
} }
const { result: _tabname = tabname } = res
setRenamingTab(null);
setTabname("");
const oldName = tabs[renamingTab]?.header;
onRenameTab?.(renamingTab, _tabname, oldName);
handleActiveChange(renamingTab);
}, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname]);
const handleCreateTab = useCallback(() => {
const res = validateTabname(tabname);
if (res.error) {
setTabnameError(`Error: ${res.error}`);
return;
}
const { result: _tabname = tabname } = res
setIsNewtabDialogOpen(false); setIsNewtabDialogOpen(false);
setTabname(""); setTabname("");
onCreateNewTab?.(_tabname); onCreateNewTab?.(_tabname);
// switch to new tab?
handleActiveChange(tabs.length, _tabname); handleActiveChange(tabs.length, _tabname);
}, [ }, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length]);
tabname,
defaultExtension,
appendDefaultExtension,
validateTabname,
onCreateNewTab,
handleActiveChange,
tabs.length,
]);
const handleCloseTab = useCallback( const handleCloseTab = useCallback(
(idx: number) => { (idx: number) => {
@@ -146,10 +187,27 @@ export const Tabs = ({
} }
onCloseTab?.(idx, tabs[idx].header); onCloseTab?.(idx, tabs[idx].header);
handleActiveChange(idx, tabs[idx].header);
}, },
[active, onCloseTab, tabs] [active, handleActiveChange, onCloseTab, tabs]
); );
const closeOption = (idx: number): Nullable<ContentMenuOption> =>
onCloseTab && {
type: "text",
label: "Close",
key: "close",
onSelect: () => handleCloseTab(idx),
};
const renameOption = (idx: number): Nullable<ContentMenuOption> =>
onRenameTab && {
type: "text",
label: "Rename",
key: "rename",
onSelect: () => setRenamingTab(idx),
};
return ( return (
<> <>
{!headless && ( {!headless && (
@@ -164,46 +222,54 @@ export const Tabs = ({
}} }}
> >
{tabs.map((tab, idx) => ( {tabs.map((tab, idx) => (
<Button <ContextMenu
key={tab.header} key={tab.header}
role="tab" options={
tabIndex={idx} [closeOption(idx), renameOption(idx)].filter(
onClick={() => handleActiveChange(idx, tab.header)} Boolean
onKeyPress={() => handleActiveChange(idx, tab.header)} ) as ContentMenuOption[]
outline={active !== idx} }
size="sm"
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
> >
{tab.header || idx} <Button
{onCloseTab && ( role="tab"
<Box tabIndex={idx}
as="span" onClick={() => handleActiveChange(idx, tab.header)}
css={{ onKeyPress={() => handleActiveChange(idx, tab.header)}
display: "flex", outline={active !== idx}
p: "2px", size="sm"
borderRadius: "$full", css={{
mr: "-4px", "&:hover": {
"&:hover": { span: {
// boxSizing: "0px 0px 1px", visibility: "visible",
backgroundColor: "$mauve2",
color: "$mauve12",
}, },
}} },
onClick={(ev: React.MouseEvent<HTMLElement>) => { }}
ev.stopPropagation(); >
handleCloseTab(idx); {tab.header || idx}
}} {onCloseTab && (
> <Box
<X size="9px" weight="bold" /> as="span"
</Box> css={{
)} display: "flex",
</Button> p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
handleCloseTab(idx);
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
</ContextMenu>
))} ))}
{onCreateNewTab && ( {onCreateNewTab && (
<Dialog <Dialog
@@ -216,11 +282,14 @@ 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 ${label.toLocaleLowerCase()}`}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle> <DialogTitle>
Create new {label.toLocaleLowerCase()}
</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>{label} name</Label> <Label>{label} name</Label>
<Input <Input
@@ -232,7 +301,7 @@ export const Tabs = ({
} }
}} }}
/> />
<ErrorText>{newtabError}</ErrorText> <ErrorText>{tabnameError}</ErrorText>
</DialogDescription> </DialogDescription>
<Flex <Flex
@@ -257,31 +326,79 @@ export const Tabs = ({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
{onRenameTab && (
<Dialog
open={renamingTab !== null}
onOpenChange={() => setRenamingTab(null)}
>
<DialogContent>
<DialogTitle>
Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre>
</DialogTitle>
<DialogDescription>
<Label>Enter new name</Label>
<Input
value={tabname}
onChange={e => setTabname(e.target.value)}
onKeyPress={e => {
if (e.key === "Enter") {
handleRenameTab();
}
}}
/>
<ErrorText>{tabnameError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleRenameTab}>
Confirm
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
</Stack> </Stack>
)} )}
{keepAllAlive ? ( {keepAllAlive
tabs.map((tab, idx) => { ? tabs.map((tab, idx) => {
// TODO Maybe rule out fragments as children // TODO Maybe rule out fragments as children
if (!isValidElement(tab.children)) { if (!isValidElement(tab.children)) {
if (active !== idx) return null; if (active !== idx) return null;
return tab.children; return tab.children;
} }
let key = tab.children.key || tab.header || idx; let key = tab.children.key || tab.header || idx;
let { children } = tab; let { children } = tab;
let { style, ...props } = children.props; let { style, ...props } = children.props;
return ( return (
<children.type <children.type
key={key} key={key}
{...props} {...props}
style={{ ...style, display: active !== idx ? "none" : undefined }} style={{
/> ...style,
); display: active !== idx ? "none" : undefined,
}) }}
) : ( />
<Fragment key={tabs[active].header || active}> );
{tabs[active].children} })
</Fragment> : tabs[active] && (
)} <Fragment key={tabs[active].header || active}>
{tabs[active].children}
</Fragment>
)}
</> </>
); );
}; };

View File

@@ -1,11 +1,14 @@
import { Play } from "phosphor-react"; import { Play } from "phosphor-react";
import { FC, useCallback, useEffect, useMemo } from "react"; import { FC, useCallback, useEffect } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../../state"; import state from "../../state";
import { import {
modifyTransaction, defaultTransactionType,
getTxFields,
modifyTxState,
prepareState, prepareState,
prepareTransaction, prepareTransaction,
SelectOption,
TransactionState, TransactionState,
} from "../../state/transactions"; } from "../../state/transactions";
import { sendTransaction } from "../../state/actions"; import { sendTransaction } from "../../state/actions";
@@ -15,7 +18,7 @@ import Flex from "../Flex";
import { TxJson } from "./json"; import { TxJson } from "./json";
import { TxUI } from "./ui"; import { TxUI } from "./ui";
import { default as _estimateFee } from "../../utils/estimateFee"; import { default as _estimateFee } from "../../utils/estimateFee";
import toast from 'react-hot-toast'; import toast from "react-hot-toast";
export interface TransactionProps { export interface TransactionProps {
header: string; header: string;
@@ -34,19 +37,18 @@ const Transaction: FC<TransactionProps> = ({
txIsDisabled, txIsDisabled,
txIsLoading, txIsLoading,
viewType, viewType,
editorSavedValue,
editorValue, editorValue,
} = txState; } = txState;
const setState = useCallback( const setState = useCallback(
(pTx?: Partial<TransactionState>) => { (pTx?: Partial<TransactionState>) => {
return modifyTransaction(header, pTx); return modifyTxState(header, pTx);
}, },
[header] [header]
); );
const prepareOptions = useCallback( const prepareOptions = useCallback(
(state: TransactionState = txState) => { (state: Partial<TransactionState> = txState) => {
const { const {
selectedTransaction, selectedTransaction,
selectedDestAccount, selectedDestAccount,
@@ -55,9 +57,7 @@ const Transaction: FC<TransactionProps> = ({
} = state; } = state;
const TransactionType = selectedTransaction?.value || null; const TransactionType = selectedTransaction?.value || null;
const Destination = const Destination = selectedDestAccount?.value || txFields?.Destination;
selectedDestAccount?.value ||
("Destination" in txFields ? null : undefined);
const Account = selectedAccount?.value || null; const Account = selectedAccount?.value || null;
return prepareTransaction({ return prepareTransaction({
@@ -109,8 +109,9 @@ const Transaction: FC<TransactionProps> = ({
} }
const options = prepareOptions(st); const options = prepareOptions(st);
if (options.Destination === null) { const fields = getTxFields(options.TransactionType);
throw Error("Destination account cannot be null"); if (fields.Destination && !options.Destination) {
throw Error("Destination account is required!");
} }
await sendTransaction(account, options, { logPrefix }); await sendTransaction(account, options, { logPrefix });
@@ -136,15 +137,38 @@ const Transaction: FC<TransactionProps> = ({
prepareOptions, prepareOptions,
]); ]);
const resetState = useCallback(() => { const getJsonString = useCallback(
modifyTransaction(header, { viewType }, { replaceState: true }); (state?: Partial<TransactionState>) =>
}, [header, viewType]); JSON.stringify(
prepareOptions?.(state) || {},
null,
editorSettings.tabSize
),
[editorSettings.tabSize, prepareOptions]
);
const jsonValue = useMemo( const resetState = useCallback(
() => (transactionType: SelectOption | undefined = defaultTransactionType) => {
editorSavedValue || const fields = getTxFields(transactionType?.value);
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize),
[editorSavedValue, editorSettings.tabSize, prepareOptions] const nwState: Partial<TransactionState> = {
viewType,
selectedTransaction: transactionType,
};
if (fields.Destination !== undefined) {
nwState.selectedDestAccount = null;
fields.Destination = "";
} else {
fields.Destination = undefined;
}
nwState.txFields = fields;
const state = modifyTxState(header, nwState, { replaceState: true });
const editorValue = getJsonString(state);
return setState({ editorValue });
},
[getJsonString, header, setState, viewType]
); );
const estimateFee = useCallback( const estimateFee = useCallback(
@@ -156,10 +180,10 @@ const Transaction: FC<TransactionProps> = ({
); );
if (!account) { if (!account) {
if (!opts?.silent) { if (!opts?.silent) {
toast.error("Please select account from the list.") toast.error("Please select account from the list.");
} }
return return;
}; }
ptx.Account = account.address; ptx.Account = account.address;
ptx.Sequence = account.sequence; ptx.Sequence = account.sequence;
@@ -176,7 +200,7 @@ const Transaction: FC<TransactionProps> = ({
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}> <Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
{viewType === "json" ? ( {viewType === "json" ? (
<TxJson <TxJson
value={jsonValue} getJsonString={getJsonString}
header={header} header={header}
state={txState} state={txState}
setState={setState} setState={setState}
@@ -199,7 +223,7 @@ const Transaction: FC<TransactionProps> = ({
<Button <Button
onClick={() => { onClick={() => {
if (viewType === "ui") { if (viewType === "ui") {
setState({ editorSavedValue: null, viewType: "json" }); setState({ viewType: "json" });
} else setState({ viewType: "ui" }); } else setState({ viewType: "ui" });
}} }}
outline outline
@@ -207,7 +231,7 @@ const Transaction: FC<TransactionProps> = ({
{viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"} {viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"}
</Button> </Button>
<Flex row> <Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}> <Button onClick={() => resetState()} outline css={{ mr: "$3" }}>
RESET RESET
</Button> </Button>
<Button <Button

View File

@@ -1,4 +1,4 @@
import { FC, useCallback, useEffect, useState } from "react"; import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state, { import state, {
prepareState, prepareState,
@@ -15,7 +15,7 @@ import Monaco from "../Monaco";
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
interface JsonProps { interface JsonProps {
value?: string; getJsonString?: (state?: Partial<TransactionState>) => string;
header?: string; header?: string;
setState: (pTx?: Partial<TransactionState> | undefined) => void; setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState; state: TransactionState;
@@ -23,22 +23,23 @@ interface JsonProps {
} }
export const TxJson: FC<JsonProps> = ({ export const TxJson: FC<JsonProps> = ({
value = "", getJsonString,
state: txState, state: txState,
header, header,
setState, setState,
}) => { }) => {
const { editorSettings, accounts } = useSnapshot(state); const { editorSettings, accounts } = useSnapshot(state);
const { editorValue = value, estimatedFee } = txState; const { editorValue, estimatedFee } = txState;
const [hasUnsaved, setHasUnsaved] = useState(false);
const [currTxType, setCurrTxType] = useState<string | undefined>( const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value txState.selectedTransaction?.value
); );
useEffect(() => { useEffect(() => {
setState({ editorValue: value }); setState({
editorValue: getJsonString?.(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]); }, []);
useEffect(() => { useEffect(() => {
const parsed = parseJSON(editorValue); const parsed = parseJSON(editorValue);
@@ -52,21 +53,22 @@ export const TxJson: FC<JsonProps> = ({
} }
}, [editorValue]); }, [editorValue]);
useEffect(() => {
if (editorValue === value) setHasUnsaved(false);
else setHasUnsaved(true);
}, [editorValue, value]);
const saveState = (value: string, transactionType?: string) => { const saveState = (value: string, transactionType?: string) => {
const tx = prepareState(value, transactionType); const tx = prepareState(value, transactionType);
if (tx) setState(tx); if (tx) {
setState(tx);
setState({
editorValue: getJsonString?.(tx),
});
}
}; };
const discardChanges = () => { const discardChanges = () => {
showAlert("Confirm", { showAlert("Confirm", {
body: "Are you sure to discard these changes?", body: "Are you sure to discard these changes?",
confirmText: "Yes", confirmText: "Yes",
onConfirm: () => setState({ editorValue: value }), onCancel: () => {},
onConfirm: () => setState({ editorValue: getJsonString?.() }),
}); });
}; };
@@ -79,8 +81,8 @@ export const TxJson: FC<JsonProps> = ({
showAlert("Error!", { showAlert("Error!", {
body: `Malformed Transaction in ${header}, would you like to discard these changes?`, body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
confirmText: "Discard", confirmText: "Discard",
onConfirm: () => setState({ editorValue: value }), onConfirm: () => setState({ editorValue: getJsonString?.() }),
onCancel: () => setState({ viewType: "json", editorSavedValue: value }), onCancel: () => setState({ viewType: "json" }),
}); });
}; };
@@ -174,6 +176,11 @@ export const TxJson: FC<JsonProps> = ({
}); });
}, [getSchemas, monacoInst]); }, [getSchemas, monacoInst]);
const hasUnsaved = useMemo(
() => editorValue !== getJsonString?.(),
[editorValue, getJsonString]
);
return ( return (
<Monaco <Monaco
rootProps={{ rootProps={{
@@ -203,14 +210,14 @@ export const TxJson: FC<JsonProps> = ({
<Flex <Flex
row row
align="center" align="center"
css={{ fontSize: "$xs", color: "$textMuted", ml: 'auto' }} css={{ fontSize: "$xs", color: "$textMuted", ml: "auto" }}
> >
<Text muted small> <Text muted small>
This file has unsaved changes. This file has unsaved changes.
</Text> </Text>
<Link <Link
css={{ ml: "$1" }} css={{ ml: "$1" }}
onClick={() => saveState(editorValue, currTxType)} onClick={() => saveState(editorValue || "", currTxType)}
> >
save save
</Link> </Link>

View File

@@ -1,4 +1,4 @@
import { FC, useCallback, useEffect, useState } from "react"; import { FC, useCallback, useEffect, useMemo, useState } from "react";
import Container from "../Container"; import Container from "../Container";
import Flex from "../Flex"; import Flex from "../Flex";
import Input from "../Input"; import Input from "../Input";
@@ -7,9 +7,10 @@ import Text from "../Text";
import { import {
SelectOption, SelectOption,
TransactionState, TransactionState,
transactionsData, transactionsOptions,
TxFields, TxFields,
getTxFields, getTxFields,
defaultTransactionType,
} from "../../state/transactions"; } from "../../state/transactions";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../../state"; import state from "../../state";
@@ -38,12 +39,6 @@ export const TxUI: FC<UIProps> = ({
txFields, txFields,
} = txState; } = txState;
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const accountOptions: SelectOption[] = accounts.map(acc => ({ const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
@@ -58,10 +53,16 @@ export const TxUI: FC<UIProps> = ({
const [feeLoading, setFeeLoading] = useState(false); const [feeLoading, setFeeLoading] = useState(false);
const resetOptions = useCallback( const resetFields = useCallback(
(tt: string) => { (tt: string) => {
const fields = getTxFields(tt); const fields = getTxFields(tt);
if (!fields.Destination) setState({ selectedDestAccount: null });
if (fields.Destination !== undefined) {
setState({ selectedDestAccount: null });
fields.Destination = "";
} else {
fields.Destination = undefined;
}
return setState({ txFields: fields }); return setState({ txFields: fields });
}, },
[setState] [setState]
@@ -102,33 +103,37 @@ export const TxUI: FC<UIProps> = ({
(tt: SelectOption) => { (tt: SelectOption) => {
setState({ selectedTransaction: tt }); setState({ selectedTransaction: tt });
const newState = resetOptions(tt.value); const newState = resetFields(tt.value);
handleEstimateFee(newState, true); handleEstimateFee(newState, true);
}, },
[handleEstimateFee, resetOptions, setState] [handleEstimateFee, resetFields, setState]
); );
const specialFields = ["TransactionType", "Account", "Destination"]; const switchToJson = () => setState({ viewType: "json" });
const otherFields = Object.keys(txFields).filter(
k => !specialFields.includes(k)
) as [keyof TxFields];
const switchToJson = () =>
setState({ editorSavedValue: null, viewType: "json" });
// default tx // default tx
useEffect(() => { useEffect(() => {
if (selectedTransaction?.value) return; if (selectedTransaction?.value) return;
const defaultOption = transactionsOptions.find( if (defaultTransactionType) {
tt => tt.value === "Payment" handleChangeTxType(defaultTransactionType);
);
if (defaultOption) {
handleChangeTxType(defaultOption);
} }
}, [handleChangeTxType, selectedTransaction?.value, transactionsOptions]); }, [handleChangeTxType, selectedTransaction?.value]);
const fields = useMemo(
() => getTxFields(selectedTransaction?.value),
[selectedTransaction?.value]
);
const specialFields = ["TransactionType", "Account"];
if (fields.Destination !== undefined) {
specialFields.push("Destination");
}
const otherFields = Object.keys(txFields).filter(
k => !specialFields.includes(k)
) as [keyof TxFields];
return ( return (
<Container <Container
@@ -185,7 +190,7 @@ export const TxUI: FC<UIProps> = ({
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/> />
</Flex> </Flex>
{txFields.Destination !== undefined && ( {fields.Destination !== undefined && (
<Flex <Flex
row row
fluid fluid

View File

@@ -55,7 +55,8 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel", "TransactionType": "EscrowCancel",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7 "OfferSequence": 7,
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -69,7 +70,8 @@
"FinishAfter": 533171558, "FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"DestinationTag": 23480, "DestinationTag": 23480,
"SourceTag": 11747 "SourceTag": 11747,
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -77,7 +79,8 @@
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7, "OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000" "Fulfillment": "A0028000",
"Fee": "10"
}, },
{ {
"TransactionType": "NFTokenMint", "TransactionType": "NFTokenMint",
@@ -117,7 +120,9 @@
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "xrp"
}, },
"Flags": 1 "Flags": 1,
"Destination": "",
"Fee": "10"
}, },
{ {
"TransactionType": "OfferCancel", "TransactionType": "OfferCancel",
@@ -165,7 +170,8 @@
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558, "CancelAfter": 533171558,
"DestinationTag": 23480, "DestinationTag": 23480,
"SourceTag": 11747 "SourceTag": 11747,
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -175,7 +181,8 @@
"$value": "200", "$value": "200",
"$type": "xrp" "$type": "xrp"
}, },
"Expiration": 543171558 "Expiration": 543171558,
"Fee": "10"
}, },
{ {
"Flags": 0, "Flags": 0,

View File

@@ -16,8 +16,9 @@
"@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-context-menu": "^0.1.6",
"@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.6",
"@radix-ui/react-id": "^0.1.1", "@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5", "@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6", "@radix-ui/react-popover": "^0.1.6",
@@ -35,7 +36,7 @@
"lodash.xor": "^4.5.0", "lodash.xor": "^4.5.0",
"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.10.1",
"next-plausible": "^3.2.0", "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",
@@ -75,5 +76,8 @@
"eslint-config-next": "11.1.2", "eslint-config-next": "11.1.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"typescript": "4.4.4" "typescript": "4.4.4"
},
"resolutions": {
"ripple-binary-codec": "=1.4.2"
} }
} }

View File

@@ -3,12 +3,12 @@ import Split from "react-split";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { Box, Container, Flex, Tab, Tabs } from "../../components"; import { Box, Container, Flex, Tab, Tabs } from "../../components";
import Transaction from "../../components/Transaction"; import Transaction from "../../components/Transaction";
import state from "../../state"; import state, { renameTxState } from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTransaction } from "../../state"; import { transactionsState, modifyTxState } from "../../state";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FileJs } from "phosphor-react"; import { FileJs } from "phosphor-react";
import RunScript from '../../components/RunScript'; import RunScript from "../../components/RunScript";
const DebugStream = dynamic(() => import("../../components/DebugStream"), { const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false, ssr: false,
@@ -96,9 +96,12 @@ const Test = () => {
keepAllAlive keepAllAlive
defaultExtension="json" defaultExtension="json"
allowedExtensions={["json"]} allowedExtensions={["json"]}
onCreateNewTab={header => modifyTransaction(header, {})} onCreateNewTab={header => modifyTxState(header, {})}
onRenameTab={(idx, nwName, oldName = "") =>
renameTxState(oldName, nwName)
}
onCloseTab={(idx, header) => onCloseTab={(idx, header) =>
header && modifyTransaction(header, undefined) header && modifyTxState(header, undefined)
} }
> >
{transactions.map(({ header, state }) => ( {transactions.map(({ header, state }) => (

View File

@@ -28,40 +28,34 @@ export const names = [
* is protected with CORS so that's why we did our own endpoint * is protected with CORS so that's why we did our own endpoint
*/ */
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => { export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
// Lets limit the number of faucet accounts to 5 for now if (typeof window === undefined) return
if (state.accounts.length > 5) {
return toast.error("You can only have maximum 6 accounts");
}
if (typeof window !== 'undefined') {
const toastId = showToast ? toast.loading("Creating account") : "";
const toastId = showToast ? toast.loading("Creating account") : ""; const res = await fetch(`${window.location.origin}/api/faucet`, {
const res = await fetch(`${window.location.origin}/api/faucet`, { method: "POST",
method: "POST", });
}); const json: FaucetAccountRes | { error: string } = await res.json();
const json: FaucetAccountRes | { error: string } = await res.json(); if ("error" in json) {
if ("error" in json) { if (showToast) {
if (showToast) { return toast.error(json.error, { id: toastId });
return toast.error(json.error, { id: toastId });
} else {
return;
}
} else { } else {
if (showToast) { return;
toast.success("New account created", { id: toastId });
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
} }
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
} }
}; };

View File

@@ -29,25 +29,30 @@ export const compileCode = async (activeId: number) => {
const file = state.files[activeId] const file = state.files[activeId]
try { try {
file.containsErrors = false file.containsErrors = false
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, { let res: Response
method: "POST", try {
headers: { res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
"Content-Type": "application/json", method: "POST",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
output: "wasm", },
compress: true, body: JSON.stringify({
strip: state.compileOptions.strip, output: "wasm",
files: [ compress: true,
{ strip: state.compileOptions.strip,
type: "c", files: [
options: state.compileOptions.optimizationLevel || '-O2', {
name: file.name, type: "c",
src: file.content, options: state.compileOptions.optimizationLevel || '-O2',
}, name: file.name,
], src: file.content,
}), },
}); ],
}),
});
} catch (error) {
throw Error("Something went wrong, check your network connection and try again!")
}
const json = await res.json(); const json = await res.json();
state.compiling = false; state.compiling = false;
if (!json.success) { if (!json.success) {
@@ -61,29 +66,34 @@ export const compileCode = async (activeId: number) => {
} }
throw errors throw errors
} }
state.logs.push({ try {
type: "success", // Decode base64 encoded wasm that is coming back from the endpoint
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`, const bufferData = await decodeBinary(json.output);
link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy", // Import wabt from and create human readable version of wasm file and
}); // put it into state
// Decode base64 encoded wasm that is coming back from the endpoint const ww = (await import('wabt')).default()
const bufferData = await decodeBinary(json.output);
file.compiledContent = ref(bufferData);
file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content
// Import wabt from and create human readable version of wasm file and
// put it into state
import("wabt").then((wabt) => {
const ww = wabt.default();
const myModule = ww.readWasm(new Uint8Array(bufferData), { const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true, readDebugNames: true,
}); });
myModule.applyNames(); myModule.applyNames();
const wast = myModule.toText({ foldExprs: false, inlineExport: false }); const wast = myModule.toText({ foldExprs: false, inlineExport: false });
state.files[state.active].compiledWatContent = wast;
toast.success("Compiled successfully!", { position: "bottom-center" }); file.compiledContent = ref(bufferData);
file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content
file.compiledWatContent = wast;
} catch (error) {
throw Error("Invalid compilation result produced, check your code for errors and try again!")
}
toast.success("Compiled successfully!", { position: "bottom-center" });
state.logs.push({
type: "success",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy",
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@@ -96,12 +106,19 @@ export const compileCode = async (activeId: number) => {
}); });
}) })
} }
else if (err instanceof Error) {
state.logs.push({
type: "error",
message: err.message,
});
}
else { else {
state.logs.push({ state.logs.push({
type: "error", type: "error",
message: "Something went wrong, check your connection try again later!", message: "Something went wrong, come back later!",
}); });
} }
state.compiling = false; state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" }); toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true file.containsErrors = true

View File

@@ -14,4 +14,11 @@ export const createNewFile = (name: string) => {
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" }; const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
state.files.push(emptyFile); state.files.push(emptyFile);
state.active = state.files.length - 1; state.active = state.files.length - 1;
};
export const renameFile = (oldName: string, nwName: string) => {
const file = state.files.find(file => file.name === oldName)
if (!file) throw Error(`No file exists with name ${oldName}`)
file.name = nwName
}; };

View File

@@ -0,0 +1,24 @@
import state, { transactionsState } from '..';
export const deleteAccount = (addr?: string) => {
if (!addr) return;
const index = state.accounts.findIndex(acc => acc.address === addr);
if (index === -1) return;
state.accounts.splice(index, 1);
// update selected accounts
transactionsState.transactions
.filter(t => t.state.selectedAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedAccount;
if (!acc) return;
acc.label = acc.value;
});
transactionsState.transactions
.filter(t => t.state.selectedDestAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedDestAccount;
if (!acc) return;
acc.label = acc.value;
});
};

View File

@@ -3,10 +3,10 @@ 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 { Link } from "../../components"; import { Link } from "../../components";
import { ref } from "valtio"; import { ref } from "valtio";
import estimateFee from "../../utils/estimateFee"; import estimateFee from "../../utils/estimateFee";
import { SetHookData } from '../../utils/setHook';
export const sha256 = async (string: string) => { export const sha256 = async (string: string) => {
const utf8 = new TextEncoder().encode(string); const utf8 = new TextEncoder().encode(string);

View File

@@ -1,6 +1,6 @@
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
import { proxy, ref, subscribe } from "valtio"; import { proxy, ref, subscribe } from "valtio";
import { devtools } from 'valtio/utils'; import { devtools, subscribeKey } from 'valtio/utils';
import { XrplClient } from "xrpl-client"; import { XrplClient } from "xrpl-client";
import { SplitSize } from "./actions/persistSplits"; import { SplitSize } from "./actions/persistSplits";
@@ -168,16 +168,23 @@ if (process.env.NODE_ENV !== "production") {
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
subscribe(state, () => { subscribe(state.accounts, () => {
const { accounts, active } = state; const { accounts } = state;
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false })) const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading)); localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
if (!state.files[active]?.compiledWatContent) {
state.activeWat = 0;
} else {
state.activeWat = active;
}
}); });
const updateActiveWat = () => {
const filename = state.files[state.active]?.name
const compiledFiles = state.files.filter(
file => file.compiledContent)
const idx = compiledFiles.findIndex(file => file.name === filename)
if (idx !== -1) state.activeWat = idx
}
subscribeKey(state, 'active', updateActiveWat)
subscribeKey(state, 'files', updateActiveWat)
} }
export default state export default state

View File

@@ -18,14 +18,13 @@ export interface TransactionState {
txIsDisabled: boolean; txIsDisabled: boolean;
txFields: TxFields; txFields: TxFields;
viewType: 'json' | 'ui', viewType: 'json' | 'ui',
editorSavedValue: null | string,
editorValue?: string, editorValue?: string,
estimatedFee?: string estimatedFee?: string
} }
export type TxFields = Omit< export type TxFields = Omit<
typeof transactionsData[0], Partial<typeof transactionsData[0]>,
"Account" | "Sequence" | "TransactionType" "Account" | "Sequence" | "TransactionType"
>; >;
@@ -36,27 +35,34 @@ export const defaultTransaction: TransactionState = {
txIsLoading: false, txIsLoading: false,
txIsDisabled: false, txIsDisabled: false,
txFields: {}, txFields: {},
viewType: 'ui', viewType: 'ui'
editorSavedValue: null
}; };
export const transactionsState = proxy({ export const transactionsState = proxy({
transactions: [ transactions: [
{ {
header: "test1.json", header: "test1.json",
state: defaultTransaction, state: { ...defaultTransaction },
}, },
], ],
activeHeader: "test1.json" activeHeader: "test1.json"
}); });
export const renameTxState = (oldName: string, nwName: string) => {
const tx = transactionsState.transactions.find(tx => tx.header === oldName);
if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`);
tx.header = nwName
}
/** /**
* Simple transaction state changer * Simple transaction state changer
* @param header Unique key and tab name for the transaction tab * @param header Unique key and tab name for the transaction tab
* @param partialTx partial transaction state, `undefined` deletes the transaction * @param partialTx partial transaction state, `undefined` deletes the transaction
* *
*/ */
export const modifyTransaction = ( export const modifyTxState = (
header: string, header: string,
partialTx?: Partial<TransactionState>, partialTx?: Partial<TransactionState>,
opts: { replaceState?: boolean } = {} opts: { replaceState?: boolean } = {}
@@ -92,7 +98,7 @@ export const modifyTransaction = (
} }
Object.keys(partialTx).forEach(k => { Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definetly safe! // Typescript mess here, but is definitely safe!
const s = tx.state as any; const s = tx.state as any;
const p = partialTx as any; // ? Make copy const p = partialTx as any; // ? Make copy
if (!deepEqual(s[k], p[k])) s[k] = p[k]; if (!deepEqual(s[k], p[k])) s[k] = p[k];
@@ -132,7 +138,7 @@ export const prepareTransaction = (data: any) => {
} }
// delete unnecessary fields // delete unnecessary fields
if (options[field] === undefined) { if (!options[field]) {
delete options[field]; delete options[field];
} }
}); });
@@ -152,7 +158,7 @@ export const prepareState = (value: string, transactionType?: string) => {
const { Account, TransactionType, Destination, ...rest } = options; const { Account, TransactionType, Destination, ...rest } = options;
let tx: Partial<TransactionState> = {}; let tx: Partial<TransactionState> = {};
const txFields = getTxFields(transactionType) const schema = getTxFields(transactionType)
if (Account) { if (Account) {
const acc = state.accounts.find(acc => acc.address === Account); const acc = state.accounts.find(acc => acc.address === Account);
@@ -180,9 +186,8 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedTransaction = null; tx.selectedTransaction = null;
} }
if (txFields.Destination !== undefined) { if (schema.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination); const dest = state.accounts.find(acc => acc.address === Destination);
rest.Destination = null
if (dest) { if (dest) {
tx.selectedDestAccount = { tx.selectedDestAccount = {
label: dest.name, label: dest.name,
@@ -199,11 +204,14 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedDestAccount = null tx.selectedDestAccount = null
} }
} }
else if (Destination) {
rest.Destination = Destination
}
Object.keys(rest).forEach(field => { Object.keys(rest).forEach(field => {
const value = rest[field]; const value = rest[field];
const origValue = txFields[field as keyof TxFields] const schemaVal = schema[field as keyof TxFields]
const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.$type === 'xrp' const isXrp = typeof value !== 'object' && schemaVal && typeof schemaVal === 'object' && schemaVal.$type === 'xrp'
if (isXrp) { if (isXrp) {
rest[field] = { rest[field] = {
$type: "xrp", $type: "xrp",
@@ -218,7 +226,6 @@ export const prepareState = (value: string, transactionType?: string) => {
}); });
tx.txFields = rest; tx.txFields = rest;
tx.editorSavedValue = null;
return tx return tx
} }
@@ -244,3 +251,10 @@ export const getTxFields = (tt?: string) => {
} }
export { transactionsData } export { transactionsData }
export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment')

View File

@@ -53,6 +53,7 @@ export const {
accent: "#9D2DFF", accent: "#9D2DFF",
background: "$gray1", background: "$gray1",
backgroundAlt: "$gray4", backgroundAlt: "$gray4",
backgroundOverlay: "$mauve2",
text: "$gray12", text: "$gray12",
textMuted: "$gray10", textMuted: "$gray10",
primary: "$plum", primary: "$plum",
@@ -365,6 +366,7 @@ export const darkTheme = createTheme("dark", {
...greenDark, ...greenDark,
...redDark, ...redDark,
deep: "rgb(10, 10, 10)", deep: "rgb(10, 10, 10)",
backgroundOverlay: "$mauve5"
// backgroundA: transparentize(0.1, grayDark.gray1), // backgroundA: transparentize(0.1, grayDark.gray1),
}, },
}); });

21
styles/keyframes.ts Normal file
View File

@@ -0,0 +1,21 @@
import { keyframes } from '../stitches.config';
export const slideUpAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
export const slideRightAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(-2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
export const slideDownAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(-2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
export const slideLeftAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});

78
utils/setHook.ts Normal file
View File

@@ -0,0 +1,78 @@
import { getTags } from './comment-parser';
import { tts, TTS } from './hookOnCalculator';
export const transactionOptions = Object.keys(tts).map(key => ({
label: key,
value: key as keyof TTS,
}));
export type SetHookData = {
Invoke: {
value: keyof TTS;
label: string;
}[];
Fee: string;
HookNamespace: string;
HookParameters: {
HookParameter: {
HookParameterName: string;
HookParameterValue: string;
};
$metaData?: any;
}[];
// HookGrants: {
// HookGrant: {
// Authorize: string;
// HookHash: string;
// };
// }[];
};
export const getParameters = (content?: string) => {
const fieldTags = ["field", "param", "arg", "argument"];
const tags = getTags(content)
.filter(tag => fieldTags.includes(tag.tag))
.filter(tag => !!tag.name);
const paramters: SetHookData["HookParameters"] = tags.map(tag => ({
HookParameter: {
HookParameterName: tag.name,
HookParameterValue: tag.default || "",
},
$metaData: {
description: tag.description,
required: !tag.optional
},
}));
return paramters;
};
export const getInvokeOptions = (content?: string) => {
const invokeTags = ["invoke", "invoke-on"];
const options = getTags(content)
.filter(tag => invokeTags.includes(tag.tag))
.reduce((cumm, curr) => {
const combined = curr.type || `${curr.name} ${curr.description}`
const opts = combined.split(' ')
return cumm.concat(opts as any)
}, [] as (keyof TTS)[])
.filter(opt => Object.keys(tts).includes(opt))
const invokeOptions: SetHookData['Invoke'] = options.map(opt => ({
label: opt,
value: opt
}))
// default
if (!invokeOptions.length) {
const payment = transactionOptions.find(tx => tx.value === "ttPAYMENT")
if (payment) return [payment]
}
return invokeOptions;
};

View File

@@ -594,6 +594,18 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-context-menu@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-0.1.6.tgz#0c75f2faffec6c8697247a4b685a432b3c4d07f0"
integrity sha512-0qa6ABaeqD+WYI+8iT0jH0QLLcV8Kv0xI+mZL4FFnG4ec9H0v+yngb5cfBBfs9e/KM8mDzFFpaeegqsQlLNqyQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.1.0"
"@radix-ui/react-context" "0.1.1"
"@radix-ui/react-menu" "0.1.6"
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-context@0.1.1": "@radix-ui/react-context@0.1.1":
version "0.1.1" version "0.1.1"
resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz" resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz"
@@ -635,9 +647,9 @@
"@radix-ui/react-use-callback-ref" "0.1.0" "@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-use-escape-keydown" "0.1.0" "@radix-ui/react-use-escape-keydown" "0.1.0"
"@radix-ui/react-dropdown-menu@^0.1.1": "@radix-ui/react-dropdown-menu@^0.1.6":
version "0.1.6" version "0.1.6"
resolved "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz#3203229788cd57e552c9f19dcc7008e2b545919c"
integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg== integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
@@ -2941,10 +2953,10 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next-auth@^4.0.0-beta.5: next-auth@^4.10.1:
version "4.2.1" version "4.10.1"
resolved "https://registry.npmjs.org/next-auth/-/next-auth-4.2.1.tgz" resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.1.tgz#33b29265d12287bb2f6d267c8d415a407c27f0e9"
integrity sha512-XDtt7nqevkNf4EJ2zKAKkI+MFsURf11kx11vPwxrBYA1MHeqWwaWbGOUOI2ekNTvfAg4nTEJJUH3LV2cLrH3Tg== integrity sha512-F00vtwBdyMIIJ8IORHOAOHjVGTDEhhm9+HpB2BQ8r40WtGxqToWWLN7Z+2ZW/z2RFlo3zhcuAtUCPUzVJxtZwQ==
dependencies: dependencies:
"@babel/runtime" "^7.16.3" "@babel/runtime" "^7.16.3"
"@panva/hkdf" "^1.0.1" "@panva/hkdf" "^1.0.1"
@@ -3638,19 +3650,7 @@ ripple-address-codec@^4.2.4:
base-x "3.0.9" base-x "3.0.9"
create-hash "^1.1.2" create-hash "^1.1.2"
ripple-binary-codec@^1.1.3: ripple-binary-codec@=1.4.2, ripple-binary-codec@^0.2.4, ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.4.2:
version "1.3.2"
resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-1.3.2.tgz"
integrity sha512-8VG1vfb3EM1J7ZdPXo9E57Zv2hF4cxT64gP6rGSQzODVgMjiBCWozhN3729qNTGtHItz0e82Oix8v95vWYBQ3A==
dependencies:
assert "^2.0.0"
big-integer "^1.6.48"
buffer "5.6.0"
create-hash "^1.2.0"
decimal.js "^10.2.0"
ripple-address-codec "^4.2.3"
ripple-binary-codec@^1.4.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3" resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3"
integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w== integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w==
@@ -3685,7 +3685,7 @@ ripple-hashes@^0.3.4, ripple-hashes@latest:
bignumber.js "^4.1.0" bignumber.js "^4.1.0"
create-hash "^1.1.2" create-hash "^1.1.2"
ripple-address-codec "^3.0.4" ripple-address-codec "^3.0.4"
ripple-binary-codec "^1.4.2" ripple-binary-codec "^0.2.4"
ripple-keypairs@^1.0.3, ripple-keypairs@latest: ripple-keypairs@^1.0.3, ripple-keypairs@latest:
version "1.1.3" version "1.1.3"