Compare commits

..

1 Commits

Author SHA1 Message Date
Vaclav Barta
a82de7087d -O0 -> -O2 2022-06-22 08:47:00 +02:00
49 changed files with 2204 additions and 2795 deletions

1
.gitignore vendored
View File

@@ -32,4 +32,3 @@ yarn-error.log*
# vercel # vercel
.vercel .vercel
.vscode

View File

@@ -31,7 +31,6 @@ import transactionsData from "../content/transactions.json";
import { SetHookDialog } from "./SetHookDialog"; 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";
export const AccountDialog = ({ export const AccountDialog = ({
activeAccountAddress, activeAccountAddress,
@@ -43,12 +42,12 @@ export const AccountDialog = ({
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [showSecret, setShowSecret] = useState(false); const [showSecret, setShowSecret] = useState(false);
const activeAccount = snap.accounts.find( const activeAccount = snap.accounts.find(
account => account.address === activeAccountAddress (account) => account.address === activeAccountAddress
); );
return ( return (
<Dialog <Dialog
open={Boolean(activeAccountAddress)} open={Boolean(activeAccountAddress)}
onOpenChange={open => { onOpenChange={(open) => {
setShowSecret(false); setShowSecret(false);
!open && setActiveAccountAddress(null); !open && setActiveAccountAddress(null);
}} }}
@@ -100,7 +99,7 @@ export const AccountDialog = ({
tabIndex={-1} tabIndex={-1}
onClick={() => { onClick={() => {
const index = state.accounts.findIndex( const index = state.accounts.findIndex(
acc => acc.address === activeAccount?.address (acc) => acc.address === activeAccount?.address
); );
state.accounts.splice(index, 1); state.accounts.splice(index, 1);
}} }}
@@ -117,16 +116,9 @@ export const AccountDialog = ({
<Text <Text
css={{ css={{
fontFamily: "$monospace", fontFamily: "$monospace",
a: { "&:hover": { textDecoration: "underline" } },
}} }}
> >
<a {activeAccount?.address}
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noopener noreferrer"
>
{activeAccount?.address}
</a>
</Text> </Text>
</Flex> </Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}> <Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
@@ -166,7 +158,7 @@ export const AccountDialog = ({
}} }}
ghost ghost
size="xs" size="xs"
onClick={() => setShowSecret(curr => !curr)} onClick={() => setShowSecret((curr) => !curr)}
> >
{showSecret ? "Hide" : "Show"} {showSecret ? "Hide" : "Show"}
</Button> </Button>
@@ -223,11 +215,7 @@ export const AccountDialog = ({
</Button> </Button>
</Text> </Text>
</Flex> </Flex>
<Flex <Flex css={{ marginLeft: "auto" }}>
css={{
marginLeft: "auto",
}}
>
<a <a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`} href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank" target="_blank"
@@ -249,22 +237,10 @@ export const AccountDialog = ({
<Text <Text
css={{ css={{
fontFamily: "$monospace", fontFamily: "$monospace",
a: { "&:hover": { textDecoration: "underline" } },
}} }}
> >
{activeAccount && activeAccount.hooks.length > 0 {activeAccount && activeAccount.hooks.length > 0
? activeAccount.hooks.map(i => { ? activeAccount.hooks.map((i) => truncate(i, 12)).join(",")
return (
<a
key={i}
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
target="_blank"
rel="noopener noreferrer"
>
{truncate(i, 12)}
</a>
);
})
: ""} : ""}
</Text> </Text>
</Flex> </Flex>
@@ -302,7 +278,7 @@ interface AccountProps {
showHookStats?: boolean; showHookStats?: boolean;
} }
const Accounts: FC<AccountProps> = props => { const Accounts: FC<AccountProps> = (props) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState< const [activeAccountAddress, setActiveAccountAddress] = useState<
string | null string | null
@@ -310,7 +286,7 @@ const Accounts: FC<AccountProps> = props => {
useEffect(() => { useEffect(() => {
const fetchAccInfo = async () => { const fetchAccInfo = async () => {
if (snap.clientStatus === "online") { if (snap.clientStatus === "online") {
const requests = snap.accounts.map(acc => const requests = snap.accounts.map((acc) =>
snap.client?.send({ snap.client?.send({
id: `hooks-builder-req-info-${acc.address}`, id: `hooks-builder-req-info-${acc.address}`,
command: "account_info", command: "account_info",
@@ -323,7 +299,7 @@ const Accounts: FC<AccountProps> = props => {
const balance = res?.account_data?.Balance as string; const balance = res?.account_data?.Balance as string;
const sequence = res?.account_data?.Sequence as number; const sequence = res?.account_data?.Sequence as number;
const accountToUpdate = state.accounts.find( const accountToUpdate = state.accounts.find(
acc => acc.address === address (acc) => acc.address === address
); );
if (accountToUpdate) { if (accountToUpdate) {
accountToUpdate.xrp = balance; accountToUpdate.xrp = balance;
@@ -331,7 +307,7 @@ const Accounts: FC<AccountProps> = props => {
accountToUpdate.error = null; accountToUpdate.error = null;
} else { } else {
const oldAccount = state.accounts.find( const oldAccount = state.accounts.find(
acc => acc.address === res?.account (acc) => acc.address === res?.account
); );
if (oldAccount) { if (oldAccount) {
oldAccount.xrp = "0"; oldAccount.xrp = "0";
@@ -342,7 +318,7 @@ const Accounts: FC<AccountProps> = props => {
} }
} }
}); });
const objectRequests = snap.accounts.map(acc => { const objectRequests = snap.accounts.map((acc) => {
return snap.client?.send({ return snap.client?.send({
id: `hooks-builder-req-objects-${acc.address}`, id: `hooks-builder-req-objects-${acc.address}`,
command: "account_objects", command: "account_objects",
@@ -353,7 +329,7 @@ const Accounts: FC<AccountProps> = props => {
objectResponses.forEach((res: any) => { objectResponses.forEach((res: any) => {
const address = res?.account as string; const address = res?.account as string;
const accountToUpdate = state.accounts.find( const accountToUpdate = state.accounts.find(
acc => acc.address === address (acc) => acc.address === address
); );
if (accountToUpdate) { if (accountToUpdate) {
accountToUpdate.hooks = accountToUpdate.hooks =
@@ -417,7 +393,9 @@ const Accounts: FC<AccountProps> = props => {
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text> <Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
</Heading> </Heading>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}> <Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
<ImportAccountDialog type="create" /> <Button ghost size="sm" onClick={() => addFaucetAccount(true)}>
Create
</Button>
<ImportAccountDialog /> <ImportAccountDialog />
</Flex> </Flex>
</Flex> </Flex>
@@ -434,7 +412,7 @@ const Accounts: FC<AccountProps> = props => {
overflowY: "auto", overflowY: "auto",
}} }}
> >
{snap.accounts.map(account => ( {snap.accounts.map((account) => (
<Flex <Flex
column column
key={account.address + account.name} key={account.address + account.name}
@@ -487,7 +465,7 @@ const Accounts: FC<AccountProps> = props => {
{!props.hideDeployBtn && ( {!props.hideDeployBtn && (
<div <div
className="hook-deploy-button" className="hook-deploy-button"
onClick={e => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
@@ -513,71 +491,31 @@ const Accounts: FC<AccountProps> = props => {
); );
}; };
export const transactionsOptions = transactionsData.map(tx => ({ export const transactionsOptions = transactionsData.map((tx) => ({
value: tx.TransactionType, value: tx.TransactionType,
label: tx.TransactionType, label: tx.TransactionType,
})); }));
const ImportAccountDialog = ({ const ImportAccountDialog = () => {
type = "import", const [value, setValue] = useState("");
}: {
type?: "import" | "create";
}) => {
const [secret, setSecret] = useState("");
const [name, setName] = useState("");
const btnText = type === "import" ? "Import" : "Create";
const title = type === "import" ? "Import Account" : "Create Account";
const handleSubmit = async () => {
if (type === "create") {
const value = capitalize(name);
await addFaucetAccount(value, true);
setName("");
setSecret("");
return;
}
importAccount(secret, name);
setName("");
setSecret("");
};
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button ghost size="sm"> <Button ghost size="sm">
{btnText} Import
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent aria-describedby={undefined}> <DialogContent>
<DialogTitle css={{ mb: "$4" }}>{title}</DialogTitle> <DialogTitle>Import account</DialogTitle>
<Flex column> <DialogDescription>
<Box css={{ mb: "$2" }}> <Label>Add account secret</Label>
<Label> <Input
Account name <Text muted>(optional)</Text> name="secret"
</Label> type="password"
<Input value={value}
name="name" onChange={(e) => setValue(e.target.value)}
type="text" />
autoComplete="off" </DialogDescription>
autoCapitalize="on"
value={name}
onChange={e => setName(e.target.value)}
/>
</Box>
{type === "import" && (
<Box>
<Label>Account secret</Label>
<Input
required
name="secret"
type="password"
autoComplete="new-password"
value={secret}
onChange={e => setSecret(e.target.value)}
/>
</Box>
)}
</Flex>
<Flex <Flex
css={{ css={{
@@ -590,8 +528,14 @@ const ImportAccountDialog = ({
<Button outline>Cancel</Button> <Button outline>Cancel</Button>
</DialogClose> </DialogClose>
<DialogClose asChild> <DialogClose asChild>
<Button type="submit" variant="primary" onClick={handleSubmit}> <Button
{title} variant="primary"
onClick={() => {
importAccount(value);
setValue("");
}}
>
Import account
</Button> </Button>
</DialogClose> </DialogClose>
</Flex> </Flex>

View File

@@ -1,135 +0,0 @@
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

@@ -1,107 +0,0 @@
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

@@ -1,7 +1,5 @@
import { useEffect } from "react"; import { useCallback, useEffect } from "react";
import ReconnectingWebSocket, { CloseEvent } from "reconnecting-websocket";
import { proxy, ref, useSnapshot } from "valtio"; import { proxy, ref, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils";
import { Select } from "."; import { Select } from ".";
import state, { ILog, transactionsState } from "../state"; import state, { ILog, transactionsState } from "../state";
import { extractJSON } from "../utils/json"; import { extractJSON } from "../utils/json";
@@ -17,7 +15,7 @@ export interface IStreamState {
status: "idle" | "opened" | "closed"; status: "idle" | "opened" | "closed";
statusChangeTimestamp?: number; statusChangeTimestamp?: number;
logs: ILog[]; logs: ILog[];
socket?: ReconnectingWebSocket; socket?: WebSocket;
} }
export const streamState = proxy<IStreamState>({ export const streamState = proxy<IStreamState>({
@@ -26,85 +24,12 @@ export const streamState = proxy<IStreamState>({
logs: [] as ILog[], logs: [] as ILog[],
}); });
const onOpen = (account: ISelect | null) => {
if (!account) {
return;
}
// streamState.logs = [];
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account?.value}`, {
type: "success",
});
};
const onError = () => {
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
});
};
const onClose = (e: CloseEvent) => {
// 999 = closed websocket connection by switching account
if (e.code !== 4999) {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
});
}
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
// Ping returns just account address, if we get that
// response we don't need to log anything
if (event.data !== streamState.selectedAccount?.value) {
pushLog(event.data);
}
};
let interval: NodeJS.Timer | null = null;
const addListeners = (account: ISelect | null) => {
if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
return;
}
streamState.logs = [];
if (account?.value) {
if (interval) {
clearInterval(interval);
}
if (streamState.socket) {
streamState.socket?.removeEventListener("open", () => onOpen(account));
streamState.socket?.removeEventListener("close", onClose);
streamState.socket?.removeEventListener("error", onError);
streamState.socket?.removeEventListener("message", onMessage);
}
streamState.socket = ref(
new ReconnectingWebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
)
);
if (streamState.socket) {
interval = setInterval(() => {
streamState.socket?.send("");
}, 45000);
}
streamState.socket.addEventListener("open", () => onOpen(account));
streamState.socket.addEventListener("close", onClose);
streamState.socket.addEventListener("error", onError);
streamState.socket.addEventListener("message", onMessage);
}
};
subscribeKey(streamState, "selectedAccount", addListeners);
const DebugStream = () => { const DebugStream = () => {
const { selectedAccount, logs } = useSnapshot(streamState); const { selectedAccount, logs, socket } = useSnapshot(streamState);
const { activeHeader: activeTxTab } = useSnapshot(transactionsState); const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
const { accounts } = useSnapshot(state); const { accounts } = useSnapshot(state);
const accountOptions = accounts.map((acc) => ({ const accountOptions = accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
})); }));
@@ -117,21 +42,117 @@ const DebugStream = () => {
options={accountOptions} options={accountOptions}
hideSelectedOptions hideSelectedOptions
value={selectedAccount} value={selectedAccount}
onChange={(acc) => { onChange={acc => (streamState.selectedAccount = acc as any)}
streamState.socket?.close(
4999,
"Old connection closed because user switched account"
);
streamState.selectedAccount = acc as any;
}}
css={{ width: "100%" }} css={{ width: "100%" }}
/> />
</> </>
); );
useEffect(() => {
const account = selectedAccount?.value;
if (account && (!socket || !socket.url.endsWith(account))) {
socket?.close();
streamState.socket = ref(
new WebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account}`
)
);
} else if (!account && socket) {
socket.close();
streamState.socket = undefined;
}
}, [selectedAccount?.value, socket]);
const onMount = useCallback(async () => {
// deliberately using `proxy` values and not the `useSnapshot` ones to have no dep list
const acc = streamState.selectedAccount;
const status = streamState.status;
if (status === "opened" && acc) {
// fetch the missing ones
try {
const url = `https://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/recent/${acc?.value}`;
// TODO Remove after api sets cors properly
const res = await fetch("/api/proxy", {
method: "POST",
body: JSON.stringify({ url }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) return;
const body = await res.json();
if (!body?.logs) return;
const start = streamState.statusChangeTimestamp || 0;
streamState.logs = [];
pushLog(`Debug stream opened for account ${acc.value}`, {
type: "success",
});
const logs = Object.entries(body.logs).filter(([tm]) => +tm >= start);
logs.forEach(([tm, log]) => pushLog(log));
} catch (error) {
console.error(error);
}
}
}, []);
useEffect(() => {
onMount();
}, [onMount]);
useEffect(() => {
const account = selectedAccount?.value;
const socket = streamState.socket;
if (!socket) return;
const onOpen = () => {
streamState.logs = [];
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account}`, {
type: "success",
});
};
const onError = () => {
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
});
};
const onClose = (e: CloseEvent) => {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
});
streamState.selectedAccount = null;
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
pushLog(event.data);
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onClose);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
};
}, [selectedAccount?.value, socket]);
useEffect(() => { useEffect(() => {
const account = transactionsState.transactions.find( const account = transactionsState.transactions.find(
(tx) => tx.header === activeTxTab tx => tx.header === activeTxTab
)?.state.selectedAccount; )?.state.selectedAccount;
if (account && account.value !== streamState.selectedAccount?.value) if (account && account.value !== streamState.selectedAccount?.value)

View File

@@ -1,6 +1,7 @@
import React, { useState } from "react"; import React, { useRef, useState } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot, ref } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import NextLink from "next/link"; import NextLink from "next/link";
@@ -9,36 +10,31 @@ import filesize from "filesize";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import state from "../state"; 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, Tabs, Tab } from "."; import { Button, Text, Link, Flex } from ".";
import Monaco from "./Monaco";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]; const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
const DeployEditor = () => { const DeployEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state); const snap = useSnapshot(state);
const router = useRouter(); const router = useRouter();
const { theme } = useTheme(); const { theme } = useTheme();
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
const compiledFiles = snap.files.filter(file => file.compiledContent); const activeFile = snap.files[snap.active];
const activeFile = compiledFiles[snap.activeWat];
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]
@@ -47,10 +43,6 @@ const DeployEditor = () => {
? "$warning" ? "$warning"
: "$success"; : "$success";
const isContentChanged =
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
// const hasDeployErrors = activeFile && activeFile.containsErrors;
const CompiledStatView = activeFile && ( const CompiledStatView = activeFile && (
<Flex <Flex
column column
@@ -68,30 +60,15 @@ const DeployEditor = () => {
{activeFile?.lastCompiled && ( {activeFile?.lastCompiled && (
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" /> <ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
)} )}
{activeFile.compiledContent?.byteLength && ( {activeFile.compiledContent?.byteLength && (
<Text css={{ ml: "$2", color }}> <Text css={{ ml: "$2", color }}>
({filesize(activeFile.compiledContent.byteLength)}) ({filesize(activeFile.compiledContent.byteLength)})
</Text> </Text>
)} )}
</Flex> </Flex>
{activeFile.compiledContent?.byteLength &&
activeFile.compiledContent?.byteLength >= 64000 && (
<Flex css={{ flexDirection: "column", py: "$3", pb: "$1" }}>
<Text css={{ ml: "$2", color: "$error" }}>
File size is larger than 64kB, cannot set hook!
</Text>
</Flex>
)}
<Button variant="link" onClick={() => setShowContent(true)}> <Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file View as WAT-file
</Button> </Button>
{isContentChanged && (
<Text warning>
File contents were changed after last compile, compile again to
incorporate your latest changes in the build.
</Text>
)}
</Flex> </Flex>
); );
const NoContentView = !snap.loading && router.isReady && ( const NoContentView = !snap.loading && router.isReady && (
@@ -110,9 +87,8 @@ 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;
return ( return (
<Box <Box
@@ -125,7 +101,7 @@ const DeployEditor = () => {
width: "100%", width: "100%",
}} }}
> >
<EditorNavigation renderNav={renderNav} /> <EditorNavigation showWat />
<Container <Container
css={{ css={{
display: "flex", display: "flex",
@@ -139,38 +115,32 @@ const DeployEditor = () => {
) : !showContent ? ( ) : !showContent ? (
CompiledStatView CompiledStatView
) : ( ) : (
<Monaco <Editor
className="hooks-editor" className="hooks-editor"
defaultLanguage={"wat"} defaultLanguage={"wat"}
language={"wat"} language={"wat"}
path={`file://tmp/c/${activeFile?.name}.wat`} path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
value={activeFile?.compiledWatContent || ""} value={snap.files?.[snap.active]?.compiledWatContent || ""}
beforeMount={monaco => { beforeMount={(monaco) => {
monaco.languages.register({ id: "wat" }); monaco.languages.register({ id: "wat" });
monaco.languages.setLanguageConfiguration("wat", wat.config); monaco.languages.setLanguageConfiguration("wat", wat.config);
monaco.languages.setMonarchTokensProvider("wat", wat.tokens); monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}} }}
onMount={editor => { onMount={(editor, monaco) => {
editorRef.current = editor;
editor.updateOptions({ editor.updateOptions({
glyphMargin: true, glyphMargin: true,
readOnly: true, readOnly: true,
}); });
}} }}
theme={theme === "dark" ? "dark" : "light"} theme={theme === "dark" ? "dark" : "light"}
overlay={
<Flex
css={{
m: "$1",
ml: "auto",
fontSize: "$sm",
color: "$textMuted",
}}
>
<Link onClick={() => setShowContent(false)}>
Exit editor mode
</Link>
</Flex>
}
/> />
)} )}
</Container> </Container>

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: 10000, zIndex: 9999,
backgroundColor: blackA.blackA9, backgroundColor: blackA.blackA9,
position: "fixed", position: "fixed",
inset: 0, inset: 0,

View File

@@ -1,7 +1,27 @@
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,10 +1,6 @@
import React, { import React, { useState, useEffect, useCallback } from "react";
useState,
useEffect,
useRef,
ReactNode,
} from "react";
import { import {
Plus,
Share, Share,
DownloadSimple, DownloadSimple,
Gear, Gear,
@@ -32,6 +28,7 @@ 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,
@@ -51,23 +48,36 @@ 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 = ({ renderNav }: { renderNav?: () => ReactNode }) => { const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
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?", {
@@ -85,55 +95,177 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
}); });
}; };
const scrollRef = useRef<HTMLDivElement>(null); const validateFilename = useCallback(
const containerRef = useRef<HTMLDivElement>(null); (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 files = snap.files;
return ( return (
<Flex css={{ flexShrink: 0, gap: "$0" }}> <Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex <Flex
id="kissa"
ref={scrollRef}
css={{ css={{
overflowX: "scroll", overflowX: "scroll",
overflowY: "hidden",
py: "$3", py: "$3",
pb: "$0",
flex: 1, flex: 1,
"&::-webkit-scrollbar": { "&::-webkit-scrollbar": {
height: "0.3em", height: 0,
background: "rgba(0,0,0,.0)", background: "transparent",
}, },
"&::-webkit-scrollbar-gutter": "stable",
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(0,0,0,.2)",
outline: "0px",
borderRadius: "9999px",
},
scrollbarColor: "rgba(0,0,0,.2) rgba(0,0,0,0)",
scrollbarGutter: "stable",
scrollbarWidth: "thin",
".dark &": {
"&::-webkit-scrollbar": {
background: "rgba(0,0,0,.0)",
},
"&::-webkit-scrollbar-gutter": "stable",
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(255,255,255,.2)",
outline: "0px",
borderRadius: "9999px",
},
scrollbarColor: "rgba(255,255,255,.2) rgba(0,0,0,0)",
scrollbarGutter: "stable",
scrollbarWidth: "thin",
},
}}
onWheelCapture={e => {
if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY;
}
}} }}
> >
<Container css={{ flex: 1 }} ref={containerRef}> <Container css={{ flex: 1 }}>
{renderNav?.()} <Stack
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

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio"; import { useSnapshot, ref } from "valtio";
import Editor from "@monaco-editor/react";
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
import { ArrowBendLeftUp } from "phosphor-react"; import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
@@ -7,7 +8,9 @@ import { useRouter } from "next/router";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import { createNewFile, saveFile } from "../state/actions"; import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants"; import { apiHeaderFiles } from "../state/constants";
import state from "../state"; import state from "../state";
@@ -19,24 +22,14 @@ import { listen } from "@codingame/monaco-jsonrpc";
import ReconnectingWebSocket from "reconnecting-websocket"; import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs"; import docs from "../xrpl-hooks-docs/docs";
import Monaco from "./Monaco";
import { saveAllFiles } from "../state/actions/saveFile";
import { Tab, Tabs } from "./Tabs";
import { renameFile } from "../state/actions/createNewFile";
const checkWritable = (filename?: string): boolean => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
if (apiHeaderFiles.find(file => file === filename)) { const currPath = editor.getModel()?.uri.path;
return false; if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true });
} else {
editor.updateOptions({ readOnly: false });
} }
return true;
};
const validateWritability = (
editor: monaco.editor.IStandaloneCodeEditor
) => {
const filename = editor.getModel()?.uri.path.split("/").pop();
const isWritable = checkWritable(filename);
editor.updateOptions({ readOnly: !isWritable });
}; };
let decorations: { [key: string]: string[] } = {}; let decorations: { [key: string]: string[] } = {};
@@ -49,7 +42,7 @@ const setMarkers = (monacoE: typeof monaco) => {
.getModelMarkers({}) .getModelMarkers({})
// Filter out the markers that are hooks specific // Filter out the markers that are hooks specific
.filter( .filter(
marker => (marker) =>
typeof marker?.code === "string" && typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-" // Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-") marker?.code?.includes("hooks-")
@@ -63,16 +56,16 @@ const setMarkers = (monacoE: typeof monaco) => {
// Add decoration (aka extra hoverMessages) to markers in the // Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are // exact same range (location) where the markers are
const models = monacoE.editor.getModels(); const models = monacoE.editor.getModels();
models.forEach(model => { models.forEach((model) => {
decorations[model.id] = model?.deltaDecorations( decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [], decorations?.[model.id] || [],
markers markers
.filter(marker => .filter((marker) =>
marker?.resource.path marker?.resource.path
.split("/") .split("/")
.includes(`${state.files?.[state.active]?.name}`) .includes(`${state.files?.[state.active]?.name}`)
) )
.map(marker => ({ .map((marker) => ({
range: new monacoE.Range( range: new monacoE.Range(
marker.startLineNumber, marker.startLineNumber,
marker.startColumn, marker.startColumn,
@@ -120,34 +113,6 @@ const HooksEditor = () => {
setMarkers(monacoRef.current); setMarkers(monacoRef.current);
} }
}, [snap.active]); }, [snap.active]);
useEffect(() => {
return () => {
saveAllFiles();
};
}, []);
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} renameDisabled={!checkWritable(file.name)} />;
})}
</Tabs>
);
return ( return (
<Box <Box
css={{ css={{
@@ -160,18 +125,18 @@ const HooksEditor = () => {
width: "100%", width: "100%",
}} }}
> >
<EditorNavigation renderNav={renderNav} /> <EditorNavigation />
{snap.files.length > 0 && router.isReady ? ( {snap.files.length > 0 && router.isReady ? (
<Monaco <Editor
className="hooks-editor"
keepCurrentModel keepCurrentModel
defaultLanguage={file?.language} defaultLanguage={snap.files?.[snap.active]?.language}
language={file?.language} language={snap.files?.[snap.active]?.language}
path={`file:///work/c/${file?.name}`} path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
defaultValue={file?.content} defaultValue={snap.files?.[snap.active]?.content}
// onChange={val => (state.files[snap.active].content = val)} // Auto save? beforeMount={(monaco) => {
beforeMount={monaco => {
if (!snap.editorCtx) { if (!snap.editorCtx) {
snap.files.forEach(file => snap.files.forEach((file) =>
monaco.editor.createModel( monaco.editor.createModel(
file.content, file.content,
file.language, file.language,
@@ -196,22 +161,29 @@ const HooksEditor = () => {
// listen when the web socket is opened // listen when the web socket is opened
listen({ listen({
webSocket: webSocket as WebSocket, webSocket: webSocket as WebSocket,
onConnection: connection => { onConnection: (connection) => {
// create and start the language client // create and start the language client
const languageClient = createLanguageClient(connection); const languageClient = createLanguageClient(connection);
const disposable = languageClient.start(); languageClient.start();
// connection.onDispose((d) => {
connection.onClose(() => { // console.log("disposed: ", d);
try { // });
disposable.dispose(); // connection.onError((ee) => {
} catch (err) { // console.log(ee =)
console.log("err", err); // })
} // connection.onClose(() => {
}); // try {
// // disposable.stop();
// disposable.dispose();
// } catch (err) {
// console.log("err", err);
// }
// });
}, },
}); });
} }
// // hook editor to global state
// editor.updateOptions({ // editor.updateOptions({
// minimap: { // minimap: {
// enabled: false, // enabled: false,
@@ -220,6 +192,10 @@ const HooksEditor = () => {
// }); // });
if (!state.editorCtx) { if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor); state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
} }
}} }}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
@@ -247,13 +223,13 @@ const HooksEditor = () => {
}); });
// Hacky way to hide Peek menu // Hacky way to hide Peek menu
editor.onContextMenu(e => { editor.onContextMenu((e) => {
const host = const host =
document.querySelector<HTMLElement>(".shadow-root-host"); document.querySelector<HTMLElement>(".shadow-root-host");
const contextMenuItems = const contextMenuItems =
host?.shadowRoot?.querySelectorAll("li.action-item"); host?.shadowRoot?.querySelectorAll("li.action-item");
contextMenuItems?.forEach(k => { contextMenuItems?.forEach((k) => {
// If menu item contains "Peek" lets hide it // If menu item contains "Peek" lets hide it
if (k.querySelector(".action-label")?.textContent === "Peek") { if (k.querySelector(".action-label")?.textContent === "Peek") {
// @ts-expect-error // @ts-expect-error

View File

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

View File

@@ -0,0 +1,234 @@
import {
useRef,
useLayoutEffect,
ReactNode,
FC,
useState,
useCallback,
} from "react";
import { FileJs, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link";
import Container from "./Container";
import LogText from "./LogText";
import state, { ILog } from "../state";
import { Pre, Link, Heading, Button, Text, Flex, Box } from ".";
import regexifyString from "regexify-string";
import { useSnapshot } from "valtio";
import { AccountDialog } from "./Accounts";
import RunScript from "./RunScript";
interface ILogBox {
title: string;
clearLog?: () => void;
logs: ILog[];
renderNav?: () => ReactNode;
enhanced?: boolean;
showButtons?: boolean;
}
const LogBox: FC<ILogBox> = ({
title,
clearLog,
logs,
children,
renderNav,
enhanced,
showButtons = true,
}) => {
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
const snap = useSnapshot(state);
useLayoutEffect(() => {
stayScrolled();
}, [stayScrolled, logs]);
return (
<Flex
as="div"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
flex: 1,
height: "100%",
}}
>
<Container
css={{
px: 0,
height: "100%",
}}
>
<Flex
fluid
css={{
height: "48px",
alignItems: "center",
fontSize: "$sm",
fontWeight: 300,
}}
>
<Heading
as="h3"
css={{
fontWeight: 300,
m: 0,
fontSize: "11px",
color: "$mauve12",
px: "$3",
textTransform: "uppercase",
alignItems: "center",
display: "inline-flex",
gap: "$3",
mr: "$3",
}}
>
<FileJs size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
{showButtons && (
<Flex css={{ gap: "$3" }}>
{snap.files
.filter((f) => f.name.endsWith(".js"))
.map((file) => (
<RunScript file={file} key={file.name} />
))}
</Flex>
)}
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
{clearLog && (
<Button ghost size="xs" onClick={clearLog}>
<Prohibit size="14px" />
</Button>
)}
</Flex>
</Flex>
<Box
as="pre"
ref={logRef}
css={{
margin: 0,
// display: "inline-block",
display: "flex",
flexDirection: "column",
width: "100%",
height: "calc(100% - 48px)", // 100% minus the logbox header height
overflowY: "auto",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
px: "$3",
pb: "$2",
whiteSpace: "normal",
}}
>
{logs?.map((log, index) => (
<Box
as="span"
key={log.type + index}
css={{
"@hover": {
"&:hover": {
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
},
},
p: enhanced ? "$1" : undefined,
my: enhanced ? "$1" : undefined,
}}
>
<Log {...log} />
</Box>
))}
{children}
</Box>
</Container>
</Flex>
);
};
export const Log: FC<ILog> = ({
type,
timestring,
message: _message,
link,
linkText,
defaultCollapsed,
jsonData: _jsonData,
}) => {
const [expanded, setExpanded] = useState(!defaultCollapsed);
const { accounts } = useSnapshot(state);
const [dialogAccount, setDialogAccount] = useState<string | null>(null);
const enrichAccounts = useCallback(
(str?: string): ReactNode => {
if (!str || !accounts.length) return null;
const pattern = `(${accounts.map((acc) => acc.address).join("|")})`;
const res = regexifyString({
pattern: new RegExp(pattern, "gim"),
decorator: (match, idx) => {
const name = accounts.find((acc) => acc.address === match)?.name;
return (
<Link
key={match + idx}
as="a"
onClick={() => setDialogAccount(match)}
title={match}
highlighted
>
{name || match}
</Link>
);
},
input: str,
});
return <>{res}</>;
},
[accounts]
);
let message: ReactNode;
if (typeof _message === "string") {
_message = _message.trim().replace(/\n /gi, "\n");
message = enrichAccounts(_message);
} else {
message = _message;
}
const jsonData = enrichAccounts(_jsonData);
return (
<>
<AccountDialog
setActiveAccountAddress={setDialogAccount}
activeAccountAddress={dialogAccount}
/>
<LogText variant={type}>
{timestring && (
<Text muted monospace>
{timestring}{" "}
</Text>
)}
<Pre>{message} </Pre>
{link && (
<NextLink href={link} shallow passHref>
<Link as="a">{linkText}</Link>
</NextLink>
)}
{jsonData && (
<Link onClick={() => setExpanded(!expanded)} as="a">
{expanded ? "Collapse" : "Expand"}
</Link>
)}
{expanded && jsonData && <Pre block>{jsonData}</Pre>}
</LogText>
<br />
</>
);
};
export default LogBox;

View File

@@ -1,75 +0,0 @@
import Editor, { loader, EditorProps, Monaco } from "@monaco-editor/react";
import { CSS } from "@stitches/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes";
import { FC, MutableRefObject, ReactNode } from "react";
import { Flex } from ".";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
export type MonacoProps = EditorProps & {
id?: string;
rootProps?: { css: CSS } & Record<string, any>;
overlay?: ReactNode;
editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor>;
monacoRef?: MutableRefObject<typeof monaco>;
};
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const Monaco: FC<MonacoProps> = ({
id,
path = `file:///${id}`,
className = id,
language = "json",
overlay,
editorRef,
monacoRef,
beforeMount,
rootProps,
...rest
}) => {
const { theme } = useTheme();
const setTheme = (monaco: Monaco) => {
monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme("light", light as any);
};
return (
<Flex
fluid
column
{...rootProps}
css={{
position: "relative",
height: "100%",
...rootProps?.css,
}}
>
<Editor
className={className}
language={language}
path={path}
beforeMount={monaco => {
beforeMount?.(monaco);
setTheme(monaco);
}}
theme={theme === "dark" ? "dark" : "light"}
{...rest}
/>
{overlay && (
<Flex
css={{ position: "absolute", bottom: 0, right: 0, width: "100%" }}
>
{overlay}
</Flex>
)}
</Flex>
);
};
export default Monaco;

View File

@@ -30,6 +30,12 @@ import PanelBox from "./PanelBox";
import { templateFileIds } from "../state/constants"; import { templateFileIds } from "../state/constants";
import { styled } from "../stitches.config"; 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, { const ImageWrapper = styled(Flex, {
position: "relative", position: "relative",
mt: "$2", mt: "$2",
@@ -295,18 +301,66 @@ const Navigation = () => {
}, },
}} }}
> >
{Object.values(templateFileIds).map((template) => ( <PanelBox
<PanelBox as="a"
key={template.id} href={`/develop/${templateFileIds.starter}`}
as="a" >
href={`/develop/${template.id}`} <ImageWrapper>
> <Starter />
<ImageWrapper>{template.icon()}</ImageWrapper> </ImageWrapper>
<Heading>{template.name}</Heading> <Heading>Starter</Heading>
<Text>{template.description}</Text> <Text>
</PanelBox> 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>
</Flex> </Flex>
<DialogClose asChild> <DialogClose asChild>
@@ -340,8 +394,6 @@ const Navigation = () => {
height: 0, height: 0,
background: "transparent", background: "transparent",
}, },
scrollbarColor: "transparent",
scrollbarWidth: "none",
}} }}
> >
<Stack <Stack

View File

@@ -1,14 +1,10 @@
import * as Handlebars from "handlebars";
import { Play, X } from "phosphor-react"; import { Play, X } from "phosphor-react";
import { import { useCallback, useEffect, useState } from "react";
HTMLInputTypeAttribute, import state, { IFile, ILog } from "../../state";
useCallback,
useEffect,
useState,
} from "react";
import state, { IAccount, IFile, ILog } from "../../state";
import Button from "../Button"; import Button from "../Button";
import Box from "../Box"; import Box from "../Box";
import Input, { Label } from "../Input"; import Input from "../Input";
import Stack from "../Stack"; import Stack from "../Stack";
import { import {
Dialog, Dialog,
@@ -21,21 +17,16 @@ import {
import Flex from "../Flex"; import Flex from "../Flex";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import Select from "../Select"; import Select from "../Select";
import Text from "../Text";
import { saveFile } from "../../state/actions/saveFile"; import { saveFile } from "../../state/actions/saveFile";
import { getErrors, getTags } from "../../utils/comment-parser";
import toast from "react-hot-toast";
const generateHtmlTemplate = (code: string, data?: Record<string, any>) => { Handlebars.registerHelper(
let processString: string | undefined; "customize_input",
const process = { env: { NODE_ENV: "production" } } as any; function (/* dynamic arguments */) {
if (data) { return new Handlebars.SafeString(arguments[0]);
Object.keys(data).forEach(key => {
process.env[key] = data[key];
});
} }
processString = JSON.stringify(process); );
const generateHtmlTemplate = (code: string) => {
return ` return `
<html> <html>
<head> <head>
@@ -64,21 +55,8 @@ const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
parent.window.postMessage({ type: 'warning', args: args || [] }, '*'); parent.window.postMessage({ type: 'warning', args: args || [] }, '*');
warnLog.apply(console, args); warnLog.apply(console, args);
} }
var process = '${processString || "{}"}';
process = JSON.parse(process);
window.process = process
function windowErrorHandler(event) {
event.preventDefault() // to prevent automatically logging to console
console.error(event.error?.toString())
}
window.addEventListener('error', windowErrorHandler);
</script> </script>
<script type="module">
<script type="module">
${code} ${code}
</script> </script>
</head> </head>
@@ -91,58 +69,74 @@ const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
type Fields = Record< type Fields = Record<
string, string,
{ {
name: string; key: string;
value: string; value: string;
type?: "Account" | `Account.${keyof IAccount}` | HTMLInputTypeAttribute; label?: string;
description?: string; type?: string;
required?: boolean; attach?: "account_secret" | "account_address" | string;
} }
>; >;
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => { const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [templateError, setTemplateError] = useState(""); const [templateError, setTemplateError] = useState("");
const getFieldValues = useCallback(() => {
try {
const parsed = Handlebars.parse(content);
const names = parsed.body
.filter((i) => i.type === "MustacheStatement")
.map((block) => {
// @ts-expect-error
const type = block.hash?.pairs?.find((i) => i.key == "type");
// @ts-expect-error
const attach = block.hash?.pairs?.find((i) => i.key == "attach");
// @ts-expect-error
const label = block.hash?.pairs?.find((i) => i.key == "label");
const key =
// @ts-expect-error
block?.path?.original === "customize_input"
? // @ts-expect-error
block?.params?.[0].original
: // @ts-expect-error
block?.path?.original;
return {
key,
label: label?.value?.original || key,
attach: attach?.value?.original,
type: type?.value?.original,
value: "",
};
});
const defaultState: Fields = {};
if (names) {
names.forEach((field) => (defaultState[field.key] = field));
}
setTemplateError("");
return defaultState;
} catch (err) {
console.log(err);
setTemplateError("Could not parse template");
return undefined;
}
}, [content]);
// const defaultFieldValues = getFieldValues();
const [fields, setFields] = useState<Fields>({}); const [fields, setFields] = useState<Fields>({});
const [iFrameCode, setIframeCode] = useState(""); const [iFrameCode, setIframeCode] = useState("");
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const runScript = () => {
const getFields = useCallback(() => { const fieldsToSend: Record<string, string> = {};
const inputTags = ["input", "param", "arg", "argument"]; Object.entries(fields).map(([key, obj]) => {
const tags = getTags(content) fieldsToSend[key] = obj.value;
.filter(tag => inputTags.includes(tag.tag)) });
.filter(tag => !!tag.name); const template = Handlebars.compile(content, { strict: false });
let _fields = tags.map(tag => ({
name: tag.name,
value: tag.default || "",
type: tag.type,
description: tag.description,
required: !tag.optional,
}));
const fields: Fields = _fields.reduce((acc, field) => {
acc[field.name] = field;
return acc;
}, {} as Fields);
const error = getErrors(content);
if (error) setTemplateError(error.message);
else setTemplateError("");
return fields;
}, [content]);
const runScript = useCallback(() => {
try { try {
let data: any = {}; const code = template(fieldsToSend);
Object.keys(fields).forEach(key => { setIframeCode(generateHtmlTemplate(code));
data[key] = fields[key].value;
});
const template = generateHtmlTemplate(content, data);
setIframeCode(template);
state.scriptLogs = [ state.scriptLogs = [
...snap.scriptLogs,
{ type: "success", message: "Started running..." }, { type: "success", message: "Started running..." },
]; ];
} catch (err) { } catch (err) {
@@ -152,7 +146,7 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
{ type: "error", message: err?.message || "Could not parse template" }, { type: "error", message: err?.message || "Could not parse template" },
]; ];
} }
}, [content, fields, snap.scriptLogs]); };
useEffect(() => { useEffect(() => {
const handleEvent = (e: any) => { const handleEvent = (e: any) => {
@@ -169,29 +163,17 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
}, [snap.scriptLogs]); }, [snap.scriptLogs]);
useEffect(() => { useEffect(() => {
const defaultFields = getFields() || {}; const newDefaultState = getFieldValues();
setFields(defaultFields); setFields(newDefaultState || {});
}, [content, setFields, getFields]); }, [content, setFields, getFieldValues]);
const accOptions = snap.accounts?.map(acc => ({ const options = snap.accounts?.map((acc) => ({
...acc,
label: acc.name, label: acc.name,
secret: acc.secret,
address: acc.address,
value: acc.address, value: acc.address,
})); }));
const isDisabled = Object.values(fields).some(
field => field.required && !field.value
);
const handleRun = useCallback(() => {
if (isDisabled)
return toast.error("Please fill in all the required fields.");
state.scriptLogs = [];
runScript();
setIsDialogOpen(false);
}, [isDisabled, runScript]);
return ( return (
<> <>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
@@ -209,87 +191,74 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
<DialogContent> <DialogContent>
<DialogTitle>Run {name} script</DialogTitle> <DialogTitle>Run {name} script</DialogTitle>
<DialogDescription> <DialogDescription>
<Box> You are about to run scripts provided by the developer of the hook,
You are about to run scripts provided by the developer of the make sure you know what you are doing.
hook, make sure you trust the author before you continue. <br />
</Box>
{templateError && ( {templateError && (
<Box <Box
as="span" as="span"
css={{ css={{ display: "block", color: "$error", mt: "$3" }}
display: "block",
color: "$error",
mt: "$3",
whiteSpace: "pre",
}}
> >
{templateError} Error occured while parsing template, modify script and try
</Box> again!
)}
{Object.keys(fields).length > 0 && (
<Box css={{ mt: "$4", mb: 0 }}>
Fill in the following parameters to run the script.
</Box> </Box>
)} )}
<br />
{Object.keys(fields).length > 0
? `You also need to fill in following parameters to run the script`
: ""}
</DialogDescription> </DialogDescription>
<Stack css={{ width: "100%" }}> <Stack css={{ width: "100%" }}>
{Object.keys(fields).map(key => { {Object.keys(fields).map((key) => (
const { name, value, type, description, required } = fields[key]; <Box key={key} css={{ width: "100%" }}>
<label>
const isAccount = type?.startsWith("Account"); {fields[key]?.label || key}{" "}
const isAccountSecret = type === "Account.secret"; {fields[key].attach === "account_secret" &&
`(Script uses account secret)`}
const accountField = </label>
(isAccount && type?.split(".")[1]) || "address"; {fields[key].attach === "account_secret" ||
fields[key].attach === "account_address" ? (
return ( <Select
<Box key={name} css={{ width: "100%" }}> css={{ mt: "$1" }}
<Label options={options}
css={{ display: "flex", justifyContent: "space-between" }} onChange={(val: any) => {
> setFields({
<span> ...fields,
{description || name} {required && <Text error>*</Text>} [key]: {
</span> ...fields[key],
{isAccountSecret && ( value:
<Text error small css={{ alignSelf: "end" }}> fields[key].attach === "account_secret"
can access account secret key ? val.secret
</Text> : val.address,
},
});
}}
value={options.find(
(opt) =>
opt.address === fields[key].value ||
opt.secret === fields[key].value
)} )}
</Label> />
{isAccount ? ( ) : (
<Select <Input
css={{ mt: "$1" }} type={fields[key].type || "text"}
options={accOptions} value={
onChange={(val: any) => { typeof fields[key].value !== "string"
setFields({ ? // @ts-expect-error
...fields, fields[key].value.value
[key]: { : fields[key].value
...fields[key], }
value: val[accountField], css={{ mt: "$1" }}
}, onChange={(e) => {
}); setFields({
}} ...fields,
value={accOptions.find( [key]: { ...fields[key], value: e.target.value },
(acc: any) => acc[accountField] === value });
)} }}
/> />
) : ( )}
<Input </Box>
type={type || "text"} ))}
value={value}
css={{ mt: "$1" }}
onChange={e => {
setFields({
...fields,
[key]: { ...fields[key], value: e.target.value },
});
}}
/>
)}
</Box>
);
})}
<Flex <Flex
css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }} css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }}
> >
@@ -298,8 +267,16 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
</DialogClose> </DialogClose>
<Button <Button
variant="primary" variant="primary"
isDisabled={isDisabled} isDisabled={
onClick={handleRun} (Object.entries(fields).length > 0 &&
Object.entries(fields).some(([key, obj]) => !obj.value)) ||
Boolean(templateError)
}
onClick={() => {
state.scriptLogs = [];
runScript();
setIsDialogOpen(false);
}}
> >
Run script Run script
</Button> </Button>

View File

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

View File

@@ -1,6 +1,7 @@
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, Box, Text } from "."; import Button from "./Button";
import Box from "./Box";
import { Stack, Flex, Select } from "."; import { Stack, Flex, Select } from ".";
import { import {
Dialog, Dialog,
@@ -18,61 +19,46 @@ 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, { IFile, SelectOption } from "../state"; import state from "../state";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook"; import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
import estimateFee from "../utils/estimateFee"; import estimateFee from "../utils/estimateFee";
import {
getParameters, const transactionOptions = Object.keys(tts).map((key) => ({
getInvokeOptions, label: key,
transactionOptions, value: key as keyof TTS,
SetHookData, }));
} from "../utils/setHook";
import { capitalize } from "../utils/helpers"; export type SetHookData = {
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 account = snap.accounts.find((acc) => acc.address === accountAddress);
const [estimateLoading, setEstimateLoading] = useState(false);
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 => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState(
accountOptions.find(acc => acc.value === accountAddress)
);
const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value
);
const 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,
@@ -80,26 +66,29 @@ 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: getDefaultValues(), defaultValues: {
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(() => {
if (!activeFile) return; setValue(
const defaultValues = getDefaultValues(); "HookNamespace",
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
reset(defaultValues); );
}, [activeFile, getDefaultValues, reset]); setFormInitialized(true);
}, [snap.activeWat, snap.files, setValue]);
useEffect(() => { useEffect(() => {
if ( if (
watchedFee && watchedFee &&
@@ -117,51 +106,45 @@ 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(
const namespace = watch("HookNamespace", getHookNamespace()); "HookNamespace",
snap.files?.[snap.active]?.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]);
const calculateFee = useCallback(async () => { // Calcucate initial fee estimate when modal opens
if (!account) return; useEffect(() => {
if (formInitialized && account) {
const formValues = getValues(); (async () => {
const tx = await prepareDeployHookTx(account, formValues); const formValues = getValues();
if (!tx) { const tx = await prepareDeployHookTx(account, formValues);
return; if (!tx) {
return;
}
const res = await estimateFee(tx, account);
if (res && res.base_fee) {
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
}
})();
} }
const res = await estimateFee(tx, account); // eslint-disable-next-line react-hooks/exhaustive-deps
if (res && res.base_fee) { }, [formInitialized]);
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
}
}, [account, getValues, setValue]);
const tooLargeFile = () => { if (!account) {
return Boolean( return null;
activeFile?.compiledContent?.byteLength && }
activeFile?.compiledContent?.byteLength >= 64000
);
};
const onSubmit: SubmitHandler<SetHookData> = async data => { const onSubmit: SubmitHandler<SetHookData> = async (data) => {
const currAccount = state.accounts.find( const currAccount = state.accounts.find(
acc => acc.address === account?.address (acc) => acc.address === account.address
); );
if (!account) return;
if (currAccount) currAccount.isLoading = true; if (currAccount) currAccount.isLoading = true;
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;
@@ -171,14 +154,8 @@ 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={onOpenChange}> <Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
ghost ghost
@@ -186,7 +163,8 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
uppercase uppercase
variant={"secondary"} variant={"secondary"}
disabled={ disabled={
!account || account.isLoading || !activeFile || tooLargeFile() account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length
} }
> >
Set Hook Set Hook
@@ -197,21 +175,14 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<DialogTitle>Deploy configuration</DialogTitle> <DialogTitle>Deploy configuration</DialogTitle>
<DialogDescription as="div"> <DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}> <Stack css={{ width: "100%", flex: 1 }}>
<Box css={{ width: "100%" }}>
<Label>Account</Label>
<Select
instanceId="deploy-account"
placeholder="Select account"
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)}
/>
</Box>
<Box css={{ width: "100%" }}> <Box css={{ width: "100%" }}>
<Label>Invoke on transactions</Label> <Label>Invoke on transactions</Label>
<Controller <Controller
name="Invoke" name="Invoke"
control={control} control={control}
defaultValue={transactionOptions.filter(
(to) => to.label === "ttPAYMENT"
)}
render={({ field }) => ( render={({ field }) => (
<Select <Select
{...field} {...field}
@@ -228,6 +199,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Input <Input
{...register("HookNamespace", { required: true })} {...register("HookNamespace", { required: true })}
autoComplete={"off"} autoComplete={"off"}
defaultValue={
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
}
/> />
{errors.HookNamespace?.type === "required" && ( {errors.HookNamespace?.type === "required" && (
<Box css={{ display: "inline", color: "$red11" }}> <Box css={{ display: "inline", color: "$red11" }}>
@@ -247,39 +221,22 @@ 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}>
<Flex column> <Input
<Flex row> // important to include key with field's id
<Input placeholder="Parameter name"
// important to include key with field's id {...register(
placeholder="Parameter name" `HookParameters.${index}.HookParameter.HookParameterName`
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" }}> />
{capitalize(field.$metaData?.description)} <Input
</Label> placeholder="Value (hex-quoted)"
</Flex> {...register(
`HookParameters.${index}.HookParameter.HookParameterValue`
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
</Stack> </Stack>
))} ))}
<Button <Button
@@ -307,7 +264,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
type="number" type="number"
{...register("Fee", { required: true })} {...register("Fee", { required: true })}
autoComplete={"off"} autoComplete={"off"}
onKeyPress={e => { onKeyPress={(e) => {
if (e.key === "." || e.key === ",") { if (e.key === "." || e.key === ",") {
e.preventDefault(); e.preventDefault();
} }
@@ -339,9 +296,8 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
alignContent: "center", alignContent: "center",
display: "flex", display: "flex",
}} }}
onClick={async e => { onClick={async (e) => {
e.preventDefault(); e.preventDefault();
if (!account) return;
setEstimateLoading(true); setEstimateLoading(true);
const formValues = getValues(); const formValues = getValues();
try { try {
@@ -440,7 +396,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
isLoading={account?.isLoading} isLoading={account.isLoading}
> >
Set Hook Set Hook
</Button> </Button>

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, Pre, Stack, Text } from "."; import { Box, Button, Flex, Input, Label, Stack, Text } from ".";
import { import {
Dialog, Dialog,
DialogTrigger, DialogTrigger,
@@ -17,8 +17,6 @@ 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",
@@ -26,31 +24,21 @@ 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;
renameDisabled?: boolean
} }
// TODO customize messages shown // TODO customise messages shown
interface Props { interface Props {
label?: string;
activeIndex?: number; activeIndex?: number;
activeHeader?: string; activeHeader?: string;
headless?: boolean; headless?: boolean;
children: ReactElement<TabProps>[]; children: ReactElement<TabProps>[];
keepAllAlive?: boolean; keepAllAlive?: boolean;
defaultExtension?: string; defaultExtension?: string;
extensionRequired?: boolean; forceDefaultExtension?: boolean;
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;
} }
@@ -58,7 +46,6 @@ interface Props {
export const Tab = (props: TabProps) => null; export const Tab = (props: TabProps) => null;
export const Tabs = ({ export const Tabs = ({
label = "Tab",
children, children,
activeIndex, activeIndex,
activeHeader, activeHeader,
@@ -67,19 +54,15 @@ export const Tabs = ({
onCreateNewTab, onCreateNewTab,
onCloseTab, onCloseTab,
onChangeActive, onChangeActive,
onRenameTab,
headerExtraValidation,
extensionRequired,
defaultExtension = "", defaultExtension = "",
allowedExtensions, forceDefaultExtension,
}: 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 [tabnameError, setTabnameError] = useState<string | null>(null); const [newtabError, setNewtabError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
if (activeIndex) setActive(activeIndex); if (activeIndex) setActive(activeIndex);
@@ -95,45 +78,17 @@ export const Tabs = ({
// when filename changes, reset error // when filename changes, reset error
useEffect(() => { useEffect(() => {
setTabnameError(null); setNewtabError(null);
}, [tabname, setTabnameError]); }, [tabname, setNewtabError]);
const validateTabname = useCallback( const validateTabname = useCallback(
(tabname: string): { error?: string; result?: string } => { (tabname: string): { error: string | null } => {
if (!tabname) {
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)) { if (tabs.find(tab => tab.header === tabname)) {
return { error: `${capitalize(label)} name already exists.` }; return { error: "Name already exists." };
} }
if (extensionRequired && !ext) { return { error: null };
return { error: "File extension is required!" };
}
if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: "This file extension is not allowed!" };
}
if (
headerExtraValidation &&
!tabname.match(headerExtraValidation.regex)
) {
return { error: headerExtraValidation.error };
}
return { result: tabname };
}, },
[ [tabs]
allowedExtensions,
defaultExtension,
extensionRequired,
headerExtraValidation,
label,
tabs,
]
); );
const handleActiveChange = useCallback( const handleActiveChange = useCallback(
@@ -144,50 +99,31 @@ export const Tabs = ({
[onChangeActive] [onChangeActive]
); );
const handleRenameTab = useCallback(() => {
if (renamingTab === null) return;
const res = validateTabname(tabname);
if (res.error) {
setTabnameError(`Error: ${res.error}`);
return;
}
const { result: nwName = tabname } = res;
setRenamingTab(null);
setTabname("");
const oldName = tabs[renamingTab]?.header;
onRenameTab?.(renamingTab, nwName, oldName);
handleActiveChange(renamingTab, nwName);
}, [
handleActiveChange,
onRenameTab,
renamingTab,
tabname,
tabs,
validateTabname,
]);
const handleCreateTab = useCallback(() => { const handleCreateTab = useCallback(() => {
const res = validateTabname(tabname); // add default extension in case omitted
if (res.error) { let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
setTabnameError(`Error: ${res.error}`); if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = _tabname + defaultExtension;
}
const chk = validateTabname(_tabname);
if (chk.error) {
setNewtabError(`Error: ${chk.error}`);
return; 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, tabname,
defaultExtension,
forceDefaultExtension,
validateTabname,
onCreateNewTab, onCreateNewTab,
handleActiveChange, handleActiveChange,
tabs.length, tabs.length,
@@ -195,34 +131,15 @@ export const Tabs = ({
const handleCloseTab = useCallback( const handleCloseTab = useCallback(
(idx: number) => { (idx: number) => {
onCloseTab?.(idx, tabs[idx].header);
if (idx <= active && active !== 0) { if (idx <= active && active !== 0) {
const nwActive = active - 1 setActive(active - 1);
handleActiveChange(nwActive, tabs[nwActive].header);
} }
onCloseTab?.(idx, tabs[idx].header);
}, },
[active, handleActiveChange, onCloseTab, tabs] [active, onCloseTab, tabs]
); );
const closeOption = (idx: number): Nullable<ContentMenuOption> =>
onCloseTab && {
type: "text",
label: "Close",
key: "close",
onSelect: () => handleCloseTab(idx),
};
const renameOption = (idx: number, tab: TabProps): Nullable<ContentMenuOption> => {
return (
onRenameTab && !tab.renameDisabled && {
type: "text",
label: "Rename",
key: "rename",
onSelect: () => setRenamingTab(idx),
}
);
}
return ( return (
<> <>
{!headless && ( {!headless && (
@@ -237,54 +154,46 @@ export const Tabs = ({
}} }}
> >
{tabs.map((tab, idx) => ( {tabs.map((tab, idx) => (
<ContextMenu <Button
key={tab.header} key={tab.header}
options={ role="tab"
[closeOption(idx), renameOption(idx, tab)].filter( tabIndex={idx}
Boolean onClick={() => handleActiveChange(idx, tab.header)}
) as ContentMenuOption[] onKeyPress={() => handleActiveChange(idx, tab.header)}
} outline={active !== idx}
> size="sm"
<Button css={{
role="tab" "&:hover": {
tabIndex={idx} span: {
onClick={() => handleActiveChange(idx, tab.header)} visibility: "visible",
onKeyPress={() => handleActiveChange(idx, tab.header)}
outline={active !== idx}
size="sm"
css={{
"&:hover": {
span: {
visibility: "visible",
},
}, },
}} },
> }}
{tab.header || idx} >
{onCloseTab && ( {tab.header || idx}
<Box {onCloseTab && (
as="span" <Box
css={{ as="span"
display: "flex", css={{
p: "2px", display: "flex",
borderRadius: "$full", p: "2px",
mr: "-4px", borderRadius: "$full",
"&:hover": { mr: "-4px",
// boxSizing: "0px 0px 1px", "&:hover": {
backgroundColor: "$mauve2", // boxSizing: "0px 0px 1px",
color: "$mauve12", backgroundColor: "$mauve2",
}, color: "$mauve12",
}} },
onClick={(ev: React.MouseEvent<HTMLElement>) => { }}
ev.stopPropagation(); onClick={(ev: React.MouseEvent<HTMLElement>) => {
handleCloseTab(idx); ev.stopPropagation();
}} handleCloseTab(idx);
> }}
<X size="9px" weight="bold" /> >
</Box> <X size="9px" weight="bold" />
)} </Box>
</Button> )}
</ContextMenu> </Button>
))} ))}
{onCreateNewTab && ( {onCreateNewTab && (
<Dialog <Dialog
@@ -297,16 +206,13 @@ export const Tabs = ({
size="sm" size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }} css={{ alignItems: "center", px: "$2", mr: "$3" }}
> >
<Plus size="16px" />{" "} <Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
{tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle> <DialogTitle>Create new tab</DialogTitle>
Create new {label.toLocaleLowerCase()}
</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>{label} name</Label> <Label>Tabname</Label>
<Input <Input
value={tabname} value={tabname}
onChange={e => setTabname(e.target.value)} onChange={e => setTabname(e.target.value)}
@@ -316,7 +222,7 @@ export const Tabs = ({
} }
}} }}
/> />
<ErrorText>{tabnameError}</ErrorText> <ErrorText>{newtabError}</ErrorText>
</DialogDescription> </DialogDescription>
<Flex <Flex
@@ -341,79 +247,31 @@ 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={{ ...style, display: active !== idx ? "none" : undefined }}
...style, />
display: active !== idx ? "none" : undefined, );
}} })
/> ) : (
); <Fragment key={tabs[active].header || active}>
}) {tabs[active].children}
: tabs[active] && ( </Fragment>
<Fragment key={tabs[active].header || active}> )}
{tabs[active].children}
</Fragment>
)}
</> </>
); );
}; };

View File

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

View File

@@ -1,14 +1,11 @@
import { Play } from "phosphor-react"; import { Play } from "phosphor-react";
import { FC, useCallback, useEffect } from "react"; import { FC, useCallback, useEffect, useMemo } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../../state"; import state from "../../state";
import { import {
defaultTransactionType, modifyTransaction,
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";
@@ -18,7 +15,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;
@@ -37,18 +34,19 @@ 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 modifyTxState(header, pTx); return modifyTransaction(header, pTx);
}, },
[header] [header]
); );
const prepareOptions = useCallback( const prepareOptions = useCallback(
(state: Partial<TransactionState> = txState) => { (state: TransactionState = txState) => {
const { const {
selectedTransaction, selectedTransaction,
selectedDestAccount, selectedDestAccount,
@@ -57,7 +55,9 @@ const Transaction: FC<TransactionProps> = ({
} = state; } = state;
const TransactionType = selectedTransaction?.value || null; const TransactionType = selectedTransaction?.value || null;
const Destination = selectedDestAccount?.value || txFields?.Destination; const Destination =
selectedDestAccount?.value ||
("Destination" in txFields ? null : undefined);
const Account = selectedAccount?.value || null; const Account = selectedAccount?.value || null;
return prepareTransaction({ return prepareTransaction({
@@ -109,9 +109,8 @@ const Transaction: FC<TransactionProps> = ({
} }
const options = prepareOptions(st); const options = prepareOptions(st);
const fields = getTxFields(options.TransactionType); if (options.Destination === null) {
if (fields.Destination && !options.Destination) { throw Error("Destination account cannot be null");
throw Error("Destination account is required!");
} }
await sendTransaction(account, options, { logPrefix }); await sendTransaction(account, options, { logPrefix });
@@ -137,38 +136,15 @@ const Transaction: FC<TransactionProps> = ({
prepareOptions, prepareOptions,
]); ]);
const getJsonString = useCallback( const resetState = useCallback(() => {
(state?: Partial<TransactionState>) => modifyTransaction(header, { viewType }, { replaceState: true });
JSON.stringify( }, [header, viewType]);
prepareOptions?.(state) || {},
null,
editorSettings.tabSize
),
[editorSettings.tabSize, prepareOptions]
);
const resetState = useCallback( const jsonValue = useMemo(
(transactionType: SelectOption | undefined = defaultTransactionType) => { () =>
const fields = getTxFields(transactionType?.value); editorSavedValue ||
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize),
const nwState: Partial<TransactionState> = { [editorSavedValue, editorSettings.tabSize, prepareOptions]
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(
@@ -180,10 +156,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;
@@ -200,7 +176,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
getJsonString={getJsonString} value={jsonValue}
header={header} header={header}
state={txState} state={txState}
setState={setState} setState={setState}
@@ -223,7 +199,7 @@ const Transaction: FC<TransactionProps> = ({
<Button <Button
onClick={() => { onClick={() => {
if (viewType === "ui") { if (viewType === "ui") {
setState({ viewType: "json" }); setState({ editorSavedValue: null, viewType: "json" });
} else setState({ viewType: "ui" }); } else setState({ viewType: "ui" });
}} }}
outline outline
@@ -231,7 +207,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,9 @@
import { FC, useCallback, useEffect, useMemo, useState } from "react"; 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 { useSnapshot } from "valtio";
import state, { import state, {
prepareState, prepareState,
@@ -6,16 +11,21 @@ import state, {
TransactionState, TransactionState,
} from "../../state"; } from "../../state";
import Text from "../Text"; import Text from "../Text";
import { Flex, Link } from ".."; import Flex from "../Flex";
import { Link } from "..";
import { showAlert } from "../../state/actions/showAlert"; import { showAlert } from "../../state/actions/showAlert";
import { parseJSON } from "../../utils/json"; import { parseJSON } from "../../utils/json";
import { extractSchemaProps } from "../../utils/schema"; import { extractSchemaProps } from "../../utils/schema";
import amountSchema from "../../content/amount-schema.json"; import amountSchema from "../../content/amount-schema.json";
import Monaco from "../Monaco";
import type monaco from "monaco-editor"; loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
interface JsonProps { interface JsonProps {
getJsonString?: (state?: Partial<TransactionState>) => string; value?: string;
header?: string; header?: string;
setState: (pTx?: Partial<TransactionState> | undefined) => void; setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState; state: TransactionState;
@@ -23,23 +33,23 @@ interface JsonProps {
} }
export const TxJson: FC<JsonProps> = ({ export const TxJson: FC<JsonProps> = ({
getJsonString, value = "",
state: txState, state: txState,
header, header,
setState, setState,
}) => { }) => {
const { editorSettings, accounts } = useSnapshot(state); const { editorSettings, accounts } = useSnapshot(state);
const { editorValue, estimatedFee } = txState; const { editorValue = value, estimatedFee } = txState;
const { theme } = useTheme();
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({ setState({ editorValue: value });
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);
@@ -53,22 +63,21 @@ 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) { if (tx) setState(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",
onCancel: () => {}, onConfirm: () => setState({ editorValue: value }),
onConfirm: () => setState({ editorValue: getJsonString?.() }),
}); });
}; };
@@ -81,11 +90,14 @@ 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: getJsonString?.() }), onConfirm: () => setState({ editorValue: value }),
onCancel: () => setState({ viewType: "json" }), onCancel: () => setState({ viewType: "json", editorSavedValue: value }),
}); });
}; };
const path = `file:///${header}`;
const monaco = useMonaco();
const getSchemas = useCallback(async (): Promise<any[]> => { const getSchemas = useCallback(async (): Promise<any[]> => {
const txObj = transactionsData.find( const txObj = transactionsData.find(
td => td.TransactionType === currTxType td => td.TransactionType === currTxType
@@ -165,68 +177,55 @@ export const TxJson: FC<JsonProps> = ({
]; ];
}, [accounts, currTxType, estimatedFee, header]); }, [accounts, currTxType, estimatedFee, header]);
const [monacoInst, setMonacoInst] = useState<typeof monaco>();
useEffect(() => { useEffect(() => {
if (!monacoInst) return; if (!monaco) return;
getSchemas().then(schemas => { getSchemas().then(schemas => {
monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true, validate: true,
schemas, schemas,
}); });
}); });
}, [getSchemas, monacoInst]); }, [getSchemas, monaco]);
const hasUnsaved = useMemo(
() => editorValue !== getJsonString?.(),
[editorValue, getJsonString]
);
return ( return (
<Monaco <Flex
rootProps={{ fluid
css: { height: "calc(100% - 45px)" }, column
}} css={{ height: "calc(100% - 45px)", position: "relative" }}
language={"json"} >
id={header} <Editor
height="100%" className="hooks-editor"
value={editorValue} language={"json"}
onChange={val => setState({ editorValue: val })} path={path}
onMount={(editor, monaco) => { height="100%"
editor.updateOptions({ beforeMount={monaco => {
minimap: { enabled: false }, monaco.editor.defineTheme("dark", dark as any);
glyphMargin: true, monaco.editor.defineTheme("light", light as any);
tabSize: editorSettings.tabSize, }}
dragAndDrop: true, value={editorValue}
fontSize: 14, onChange={val => setState({ editorValue: val })}
}); onMount={(editor, monaco) => {
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
});
setMonacoInst(monaco); // register onExit cb
// register onExit cb const model = editor.getModel();
const model = editor.getModel(); model?.onWillDispose(() => onExit(model.getValue()));
model?.onWillDispose(() => onExit(model.getValue())); }}
}} theme={theme === "dark" ? "dark" : "light"}
overlay={ />
hasUnsaved ? ( {hasUnsaved && (
<Flex <Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
row This file has unsaved changes.{" "}
align="center" <Link onClick={() => saveState(editorValue, currTxType)}>save</Link>{" "}
css={{ fontSize: "$xs", color: "$textMuted", ml: "auto" }} <Link onClick={discardChanges}>discard</Link>
> </Text>
<Text muted small> )}
This file has unsaved changes. </Flex>
</Text>
<Link
css={{ ml: "$1" }}
onClick={() => saveState(editorValue || "", currTxType)}
>
save
</Link>
<Link css={{ ml: "$1" }} onClick={discardChanges}>
discard
</Link>
</Flex>
) : undefined
}
/>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { FC, useCallback, useEffect, 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,10 +7,9 @@ import Text from "../Text";
import { import {
SelectOption, SelectOption,
TransactionState, TransactionState,
transactionsOptions, transactionsData,
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";
@@ -39,6 +38,11 @@ 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,
@@ -53,16 +57,10 @@ export const TxUI: FC<UIProps> = ({
const [feeLoading, setFeeLoading] = useState(false); const [feeLoading, setFeeLoading] = useState(false);
const resetFields = useCallback( const resetOptions = 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]
@@ -99,42 +97,33 @@ export const TxUI: FC<UIProps> = ({
[estimateFee, handleSetField] [estimateFee, handleSetField]
); );
const handleChangeTxType = useCallback( const handleChangeTxType = (tt: SelectOption) => {
(tt: SelectOption) => { setState({ selectedTransaction: tt });
setState({ selectedTransaction: tt });
const newState = resetFields(tt.value); const newState = resetOptions(tt.value);
handleEstimateFee(newState, true); handleEstimateFee(newState, true);
}, };
[handleEstimateFee, resetFields, setState]
);
const switchToJson = () => setState({ viewType: "json" }); const specialFields = ["TransactionType", "Account", "Destination"];
// default tx
useEffect(() => {
if (selectedTransaction?.value) return;
if (defaultTransactionType) {
handleChangeTxType(defaultTransactionType);
}
}, [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( const otherFields = Object.keys(txFields).filter(
k => !specialFields.includes(k) k => !specialFields.includes(k)
) as [keyof TxFields]; ) as [keyof TxFields];
const switchToJson = () =>
setState({ editorSavedValue: null, viewType: "json" });
useEffect(() => {
const defaultOption = transactionsOptions.find(
tt => tt.value === "Payment"
);
if (defaultOption) {
handleChangeTxType(defaultOption);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<Container <Container
css={{ css={{
@@ -190,7 +179,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>
{fields.Destination !== undefined && ( {txFields.Destination !== undefined && (
<Flex <Flex
row row
fluid fluid
@@ -264,39 +253,13 @@ export const TxUI: FC<UIProps> = ({
/> />
) : ( ) : (
<Input <Input
type={isFee ? "number" : "text"}
value={value} value={value}
onChange={e => { onChange={e => {
if (isFee) { handleSetField(field, e.target.value);
const val = e.target.value
.replaceAll(".", "")
.replaceAll(",", "");
handleSetField(field, val);
} else {
handleSetField(field, e.target.value);
}
}} }}
onKeyPress={
isFee
? e => {
if (e.key === "." || e.key === ",") {
e.preventDefault();
}
}
: undefined
}
css={{ css={{
width: "70%", width: "70%",
flex: "inherit", flex: "inherit",
"-moz-appearance": "textfield",
"&::-webkit-outer-spin-button": {
"-webkit-appearance": "none",
margin: 0,
},
"&::-webkit-inner-spin-button ": {
"-webkit-appearance": "none",
margin: 0,
},
}} }}
/> />
)} )}
@@ -305,8 +268,6 @@ export const TxUI: FC<UIProps> = ({
size="xs" size="xs"
variant="primary" variant="primary"
outline outline
disabled={txState.txIsDisabled}
isDisabled={txState.txIsDisabled}
isLoading={feeLoading} isLoading={feeLoading}
css={{ css={{
position: "absolute", position: "absolute",

View File

@@ -40,9 +40,9 @@
{ {
"label": "Token", "label": "Token",
"body": { "body": {
"currency": "${1:USD}", "currency": "${1:13.1}",
"value": "${2:100}", "value": "${2:FOO}",
"issuer": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}" "description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
} }
} }
] ]

View File

@@ -55,8 +55,7 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel", "TransactionType": "EscrowCancel",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7, "OfferSequence": 7
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -70,8 +69,7 @@
"FinishAfter": 533171558, "FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"DestinationTag": 23480, "DestinationTag": 23480,
"SourceTag": 11747, "SourceTag": 11747
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -79,50 +77,32 @@
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7, "OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000", "Fulfillment": "A0028000"
"Fee": "10"
},
{
"TransactionType": "NFTokenMint",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenTaxon": 0,
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
}, },
{ {
"TransactionType": "NFTokenBurn", "TransactionType": "NFTokenBurn",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10", "Fee": "10",
"NFTokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65" "TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
}, },
{ {
"TransactionType": "NFTokenAcceptOffer", "TransactionType": "NFTokenAcceptOffer",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", "Fee": "10"
"Fee": "10",
"NFTokenSellOffer": "A2FA1A9911FE2AEF83DAB05F437768E26A301EF899BD31EB85E704B3D528FF18",
"NFTokenBuyOffer": "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862",
"NFTokenBrokerFee": "10"
}, },
{ {
"TransactionType": "NFTokenCancelOffer", "TransactionType": "NFTokenCancelOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "10", "TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
"NFTokenOffers": {
"$type": "json",
"$value": ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"]
}
}, },
{ {
"TransactionType": "NFTokenCreateOffer", "TransactionType": "NFTokenCreateOffer",
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX", "Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007", "TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "xrp"
}, },
"Flags": 1, "Flags": 1
"Destination": "",
"Fee": "10"
}, },
{ {
"TransactionType": "OfferCancel", "TransactionType": "OfferCancel",
@@ -170,8 +150,7 @@
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558, "CancelAfter": 533171558,
"DestinationTag": 23480, "DestinationTag": 23480,
"SourceTag": 11747, "SourceTag": 11747
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -181,8 +160,7 @@
"$value": "200", "$value": "200",
"$type": "xrp" "$type": "xrp"
}, },
"Expiration": 543171558, "Expiration": 543171558
"Fee": "10"
}, },
{ {
"Flags": 0, "Flags": 0,
@@ -234,13 +212,9 @@
"Fee": "12", "Fee": "12",
"Flags": 262144, "Flags": 262144,
"LastLedgerSequence": 8007750, "LastLedgerSequence": 8007750,
"LimitAmount": { "Amount": {
"$type": "json", "$value": "100",
"$value": { "$type": "xrp"
"currency": "USD",
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
"value": "100"
}
}, },
"Sequence": 12 "Sequence": 12
} }

View File

@@ -8,6 +8,9 @@ module.exports = {
config.resolve.alias["vscode"] = require.resolve( config.resolve.alias["vscode"] = require.resolve(
"@codingame/monaco-languageclient/lib/vscode-compatibility" "@codingame/monaco-languageclient/lib/vscode-compatibility"
); );
config.resolve.alias["handlebars"] = require.resolve(
"handlebars/dist/handlebars.js"
);
if (!isServer) { if (!isServer) {
config.resolve.fallback.fs = false; config.resolve.fallback.fs = false;
} }

View File

@@ -16,9 +16,8 @@
"@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.6", "@radix-ui/react-dropdown-menu": "^0.1.1",
"@radix-ui/react-id": "^0.1.1", "@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5", "@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6", "@radix-ui/react-popover": "^0.1.6",
@@ -26,18 +25,17 @@
"@radix-ui/react-tooltip": "^0.1.7", "@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.8",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"comment-parser": "^1.3.1",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"filesize": "^8.0.7", "filesize": "^8.0.7",
"handlebars": "^4.7.7",
"javascript-time-ago": "^2.3.11", "javascript-time-ago": "^2.3.11",
"jszip": "^3.7.1", "jszip": "^3.7.1",
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
"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.10.3", "next-auth": "^4.0.0-beta.5",
"next-plausible": "^3.2.0",
"next-themes": "^0.1.1", "next-themes": "^0.1.1",
"normalize-url": "^7.0.2", "normalize-url": "^7.0.2",
"octokit": "^1.7.0", "octokit": "^1.7.0",
@@ -62,7 +60,7 @@
"vscode-languageserver": "^7.0.0", "vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2", "vscode-uri": "^3.0.2",
"wabt": "1.0.16", "wabt": "1.0.16",
"xrpl-accountlib": "^1.5.2", "xrpl-accountlib": "^1.3.2",
"xrpl-client": "^1.9.4" "xrpl-client": "^1.9.4"
}, },
"devDependencies": { "devDependencies": {
@@ -76,8 +74,5 @@
"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

@@ -7,7 +7,6 @@ import { ThemeProvider } from "next-themes";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { IdProvider } from "@radix-ui/react-id"; import { IdProvider } from "@radix-ui/react-id";
import PlausibleProvider from "next-plausible";
import { darkTheme, css } from "../stitches.config"; import { darkTheme, css } from "../stitches.config";
import Navigation from "../components/Navigation"; import Navigation from "../components/Navigation";
@@ -18,8 +17,6 @@ import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en.json"; import en from "javascript-time-ago/locale/en.json";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import Alert from "../components/AlertDialog"; import Alert from "../components/AlertDialog";
import { Button, Flex } from "../components";
import { ChatCircleText } from "phosphor-react";
TimeAgo.setDefaultLocale(en.locale); TimeAgo.setDefaultLocale(en.locale);
TimeAgo.addLocale(en); TimeAgo.addLocale(en);
@@ -40,7 +37,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
if ( if (
!gistId && !gistId &&
router.isReady && router.isReady &&
router.pathname.includes("/develop") && !router.pathname.includes("/sign-in") &&
!snap.files.length && !snap.files.length &&
!snap.mainModalShowed !snap.mainModalShowed
) { ) {
@@ -117,7 +114,6 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
media="(prefers-color-scheme: light)" media="(prefers-color-scheme: light)"
/> />
</Head> </Head>
<IdProvider> <IdProvider>
<SessionProvider session={session}> <SessionProvider session={session}>
<ThemeProvider <ThemeProvider
@@ -129,40 +125,23 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
dark: darkTheme.className, dark: darkTheme.className,
}} }}
> >
<PlausibleProvider <Navigation />
domain="hooks-builder.xrpl.org" <Component {...pageProps} />
trackOutboundLinks <Toaster
> toastOptions={{
<Navigation /> className: css({
<Component {...pageProps} /> backgroundColor: "$mauve1",
<Toaster color: "$mauve10",
toastOptions={{ fontSize: "$sm",
className: css({ zIndex: 9999,
backgroundColor: "$mauve1", ".dark &": {
color: "$mauve10", backgroundColor: "$mauve4",
fontSize: "$sm", color: "$mauve12",
zIndex: 9999, },
".dark &": { })(),
backgroundColor: "$mauve4", }}
color: "$mauve12", />
}, <Alert />
})(),
}}
/>
<Alert />
<Flex
as="a"
href="https://github.com/XRPLF/Hooks/discussions"
target="_blank"
rel="noopener noreferrer"
css={{ position: "fixed", right: "$4", bottom: "$4" }}
>
<Button size="sm" variant="primary" outline>
<ChatCircleText size={14} style={{ marginRight: "0px" }} />
Bugs & Discussions
</Button>
</Flex>
</PlausibleProvider>
</ThemeProvider> </ThemeProvider>
</SessionProvider> </SessionProvider>
</IdProvider> </IdProvider>

View File

@@ -1,13 +1,14 @@
import { Label } from "@radix-ui/react-label"; import { Label } from "@radix-ui/react-label";
import type { NextPage } from "next"; import type { NextPage } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { FileJs, Gear, Play } from "phosphor-react"; import { Gear, Play } from "phosphor-react";
import Hotkeys from "react-hot-keys"; import Hotkeys from "react-hot-keys";
import Split from "react-split"; import Split from "react-split";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { ButtonGroup, Flex } from "../../components"; import { ButtonGroup, Flex } from "../../components";
import Box from "../../components/Box"; import Box from "../../components/Box";
import Button from "../../components/Button"; import Button from "../../components/Button";
import LogBoxForScripts from "../../components/LogBoxForScripts";
import Popover from "../../components/Popover"; import Popover from "../../components/Popover";
import RunScript from "../../components/RunScript"; import RunScript from "../../components/RunScript";
import state from "../../state"; import state from "../../state";
@@ -243,8 +244,8 @@ const Home: NextPage = () => {
flex: 1, flex: 1,
}} }}
> >
<LogBox <LogBoxForScripts
Icon={FileJs} showButtons={false}
title="Script Log" title="Script Log"
logs={snap.scriptLogs} logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])} clearLog={() => (state.scriptLogs = [])}

View File

@@ -3,12 +3,11 @@ 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, { renameTxState } from "../../state"; import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTxState } from "../../state"; import { transactionsState, modifyTransaction } from "../../state";
import LogBoxForScripts from "../../components/LogBoxForScripts";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FileJs } from "phosphor-react";
import RunScript from "../../components/RunScript";
const DebugStream = dynamic(() => import("../../components/DebugStream"), { const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false, ssr: false,
@@ -33,35 +32,19 @@ const Test = () => {
if (!showComponent) { if (!showComponent) {
return null; return null;
} }
const hasScripts = Boolean( const hasScripts =
snap.files.filter(f => f.name.toLowerCase()?.endsWith(".js")).length snap.files.filter((f) => f.name.endsWith(".js")).length > 0;
);
const renderNav = () => (
<Flex css={{ gap: "$3" }}>
{snap.files
.filter(f => f.name.endsWith(".js"))
.map(file => (
<RunScript file={file} key={file.name} />
))}
</Flex>
);
return ( return (
<Container css={{ px: 0 }}> <Container css={{ px: 0 }}>
<Split <Split
direction="vertical" direction="vertical"
sizes={ sizes={
hasScripts && getSplit("testVertical")?.length === 2 getSplit("testVertical") || (hasScripts ? [50, 20, 30] : [50, 50])
? [50, 20, 30]
: hasScripts
? [50, 20, 50]
: [50, 50]
} }
gutterSize={4} gutterSize={4}
gutterAlign="center" gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }} style={{ height: "calc(100vh - 60px)" }}
onDragEnd={e => saveSplit("testVertical", e)} onDragEnd={(e) => saveSplit("testVertical", e)}
> >
<Flex <Flex
row row
@@ -83,25 +66,21 @@ const Test = () => {
width: "100%", width: "100%",
height: "100%", height: "100%",
}} }}
onDragEnd={e => saveSplit("testHorizontal", e)} onDragEnd={(e) => saveSplit("testHorizontal", e)}
> >
<Box css={{ width: "55%", px: "$2" }}> <Box css={{ width: "55%", px: "$2" }}>
<Tabs <Tabs
label="Transaction"
activeHeader={activeHeader} activeHeader={activeHeader}
// TODO make header a required field // TODO make header a required field
onChangeActive={(idx, header) => { onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header; if (header) transactionsState.activeHeader = header;
}} }}
keepAllAlive keepAllAlive
defaultExtension="json" forceDefaultExtension
allowedExtensions={["json"]} defaultExtension=".json"
onCreateNewTab={header => modifyTxState(header, {})} onCreateNewTab={(header) => modifyTransaction(header, {})}
onRenameTab={(idx, nwName, oldName = "") =>
renameTxState(oldName, nwName)
}
onCloseTab={(idx, header) => onCloseTab={(idx, header) =>
header && modifyTxState(header, undefined) header && modifyTransaction(header, undefined)
} }
> >
{transactions.map(({ header, state }) => ( {transactions.map(({ header, state }) => (
@@ -116,7 +95,7 @@ const Test = () => {
</Box> </Box>
</Split> </Split>
</Flex> </Flex>
{hasScripts ? ( {hasScripts && (
<Flex <Flex
as="div" as="div"
css={{ css={{
@@ -125,15 +104,13 @@ const Test = () => {
flexDirection: "column", flexDirection: "column",
}} }}
> >
<LogBox <LogBoxForScripts
Icon={FileJs}
title="Helper scripts" title="Helper scripts"
logs={snap.scriptLogs} logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])} clearLog={() => (state.scriptLogs = [])}
renderNav={renderNav}
/> />
</Flex> </Flex>
) : null} )}
<Flex> <Flex>
<Split <Split
direction="horizontal" direction="horizontal"

View File

@@ -27,35 +27,41 @@ export const names = [
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint * new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
* 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 (showToast: boolean = false) => {
if (typeof window === undefined) return // Lets limit the number of faucet accounts to 5 for now
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 res = await fetch(`${window.location.origin}/api/faucet`, { const toastId = showToast ? toast.loading("Creating account") : "";
method: "POST", const res = await fetch(`${window.location.origin}/api/faucet`, {
}); method: "POST",
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
if (showToast) {
return toast.error(json.error, { id: toastId });
} else {
return;
}
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
}
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'
}); });
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
if (showToast) {
return toast.error(json.error, { id: toastId });
} else {
return;
}
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
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

@@ -14,113 +14,80 @@ import { ref } from "valtio";
*/ */
export const compileCode = async (activeId: number) => { export const compileCode = async (activeId: number) => {
// Save the file to global state // Save the file to global state
saveFile(false, activeId); saveFile(false);
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) { if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error("Missing env!"); throw Error("Missing env!");
} }
// Bail out if we're already compiling // Bail out if we're already compiling
if (state.compiling) { if (state.compiling) {
// if compiling is ongoing return // TODO Inform user about it. // if compiling is ongoing return
return; return;
} }
// Set loading state to true // Set loading state to true
state.compiling = true; state.compiling = true;
state.logs = [] state.logs = []
const file = state.files[activeId]
try { try {
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: state.files[activeId].name,
type: "c", src: state.files[activeId].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) {
const errors = [json.message] state.logs.push({ type: "error", message: json.message });
if (json.tasks && json.tasks.length > 0) { if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => { json.tasks.forEach((task: any) => {
if (!task.success) { if (!task.success) {
errors.push(task?.console) state.logs.push({ type: "error", message: task?.console });
} }
}); });
} }
throw errors return toast.error(`Couldn't compile!`, { position: "bottom-center" });
} }
try {
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
// Import wabt from and create human readable version of wasm file and
// put it into state
const ww = (await import('wabt')).default()
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true,
});
myModule.applyNames();
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
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({ state.logs.push({
type: "success", type: "success",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`, message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace("develop", "deploy"), link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy", linkText: "Go to deploy",
}); });
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
state.files[state.active].compiledContent = ref(bufferData);
state.files[state.active].lastCompiled = new Date();
// Import wabt from and create human readable version of wasm file and
// put it into state
import("wabt").then((wabt) => {
const ww = wabt.default();
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true,
});
myModule.applyNames();
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
state.files[state.active].compiledWatContent = wast;
toast.success("Compiled successfully!", { position: "bottom-center" });
});
} catch (err) { } catch (err) {
console.log(err); console.log(err);
state.logs.push({
if (err instanceof Array && typeof err[0] === 'string') { type: "error",
err.forEach(message => { message: "Error occured while compiling!",
state.logs.push({ });
type: "error",
message,
});
})
}
else if (err instanceof Error) {
state.logs.push({
type: "error",
message: err.message,
});
}
else {
state.logs.push({
type: "error",
message: "Something went wrong, come back later!",
});
}
state.compiling = false; state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true
} }
}; };

View File

@@ -14,11 +14,4 @@ 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

@@ -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);
@@ -54,15 +54,15 @@ export const prepareDeployHookTx = async (
account: IAccount & { name?: string }, account: IAccount & { name?: string },
data: SetHookData data: SetHookData
) => { ) => {
const activeFile = state.files[state.active]?.compiledContent if (
? state.files[state.active] !state.files ||
: state.files.filter((file) => file.compiledContent)[0]; state.files.length === 0 ||
!state.files?.[state.active]?.compiledContent
if (!state.files || state.files.length === 0) { ) {
return; return;
} }
if (!activeFile?.compiledContent) { if (!state.files?.[state.active]?.compiledContent) {
return; return;
} }
if (!state.client) { if (!state.client) {
@@ -99,7 +99,7 @@ export const prepareDeployHookTx = async (
{ {
Hook: { Hook: {
CreateCode: arrayBufferToHex( CreateCode: arrayBufferToHex(
activeFile?.compiledContent state.files?.[state.active]?.compiledContent
).toUpperCase(), ).toUpperCase(),
HookOn: calculateHookOn(hookOnValues), HookOn: calculateHookOn(hookOnValues),
HookNamespace, HookNamespace,
@@ -126,10 +126,6 @@ export const deployHook = async (
data: SetHookData data: SetHookData
) => { ) => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active]
: state.files.filter((file) => file.compiledContent)[0];
state.deployValues[activeFile.name] = data;
const tx = await prepareDeployHookTx(account, data); const tx = await prepareDeployHookTx(account, data);
if (!tx) { if (!tx) {
return; return;
@@ -189,7 +185,7 @@ export const deployHook = async (
console.log(err); console.log(err);
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: "Error occurred while deploying", message: "Error occured while deploying",
}); });
} }
if (currentAccount) { if (currentAccount) {
@@ -272,10 +268,10 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
toast.error("Error occurred while deleting hook", { id: toastId }); toast.error("Error occured while deleting hoook", { id: toastId });
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: "Error occurred while deleting hook", message: "Error occured while deleting hook",
}); });
} }
if (currentAccount) { if (currentAccount) {

View File

@@ -13,7 +13,7 @@ export const downloadAsZip = async () => {
const zipFileName = guessZipFileName(files); const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName); zipped.saveFile(zipFileName);
} catch (error) { } catch (error) {
toast.error('Error occurred while creating zip file, try again later') toast.error('Error occured while creating zip file, try again later')
} finally { } finally {
state.zipLoading = false state.zipLoading = false
} }

View File

@@ -19,7 +19,7 @@ export const fetchFiles = (gistId: string) => {
octokit octokit
.request("GET /gists/{gist_id}", { gist_id: gistId }) .request("GET /gists/{gist_id}", { gist_id: gistId })
.then(async res => { .then(async res => {
if (!Object.values(templateFileIds).map(v => v.id).includes(gistId)) { if (!Object.values(templateFileIds).includes(gistId)) {
return res return res
} }
// in case of templates, fetch header file(s) and append to res // in case of templates, fetch header file(s) and append to res

View File

@@ -5,7 +5,7 @@ import state from '../index';
import { names } from './addFaucetAccount'; import { names } from './addFaucetAccount';
// Adds test account to global state with secret key // Adds test account to global state with secret key
export const importAccount = (secret: string, name?: string) => { export const importAccount = (secret: string) => {
if (!secret) { if (!secret) {
return toast.error("You need to add secret!"); return toast.error("You need to add secret!");
} }
@@ -19,7 +19,7 @@ export const importAccount = (secret: string, name?: string) => {
if (err?.message) { if (err?.message) {
toast.error(err.message) toast.error(err.message)
} else { } else {
toast.error('Error occurred while importing account') toast.error('Error occured while importing account')
} }
return; return;
} }
@@ -27,7 +27,7 @@ export const importAccount = (secret: string, name?: string) => {
return toast.error(`Couldn't create account!`); return toast.error(`Couldn't create account!`);
} }
state.accounts.push({ state.accounts.push({
name: name || names[state.accounts.length], name: names[state.accounts.length],
address: account.address || "", address: account.address || "",
secret: account.secret.familySeed || "", secret: account.secret.familySeed || "",
xrp: "0", xrp: "0",

View File

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

View File

@@ -24,6 +24,7 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
Fee, // TODO auto-fillable default Fee, // TODO auto-fillable default
...opts ...opts
}; };
const { logPrefix = '' } = options || {} const { logPrefix = '' } = options || {}
try { try {
const signedAccount = derive.familySeed(account.secret); const signedAccount = derive.familySeed(account.secret);

View File

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

View File

@@ -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, subscribeKey } from 'valtio/utils'; import { devtools } from 'valtio/utils';
import { XrplClient } from "xrpl-client"; import { XrplClient } from "xrpl-client";
import { SplitSize } from "./actions/persistSplits"; import { SplitSize } from "./actions/persistSplits";
@@ -13,11 +13,9 @@ export interface IFile {
name: string; name: string;
language: string; language: string;
content: string; content: string;
compiledValueSnapshot?: string
compiledContent?: ArrayBuffer | null; compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null; compiledWatContent?: string | null;
lastCompiled?: Date lastCompiled?: Date
containsErrors?: boolean
} }
export interface FaucetAccountRes { export interface FaucetAccountRes {
@@ -54,8 +52,6 @@ export interface ILog {
defaultCollapsed?: boolean defaultCollapsed?: boolean
} }
export type DeployValue = Record<IFile['name'], any>;
export interface IState { export interface IState {
files: IFile[]; files: IFile[];
gistId?: string | null; gistId?: string | null;
@@ -86,8 +82,7 @@ export interface IState {
compileOptions: { compileOptions: {
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os'; optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
strip: boolean strip: boolean
}, }
deployValues: DeployValue
} }
// let localStorageState: null | string = null; // let localStorageState: null | string = null;
@@ -121,8 +116,7 @@ let initialState: IState = {
compileOptions: { compileOptions: {
optimizationLevel: '-O2', optimizationLevel: '-O2',
strip: true strip: true
}, }
deployValues: {}
}; };
let localStorageAccounts: string | null = null; let localStorageAccounts: string | null = null;
@@ -168,23 +162,16 @@ if (process.env.NODE_ENV !== "production") {
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
subscribe(state.accounts, () => { subscribe(state, () => {
const { accounts } = state; const { accounts, active } = 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,13 +18,14 @@ 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<
Partial<typeof transactionsData[0]>, typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType" "Account" | "Sequence" | "TransactionType"
>; >;
@@ -35,34 +36,27 @@ 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 modifyTxState = ( export const modifyTransaction = (
header: string, header: string,
partialTx?: Partial<TransactionState>, partialTx?: Partial<TransactionState>,
opts: { replaceState?: boolean } = {} opts: { replaceState?: boolean } = {}
@@ -98,7 +92,7 @@ export const modifyTxState = (
} }
Object.keys(partialTx).forEach(k => { Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definitely safe! // Typescript mess here, but is definetly 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];
@@ -124,7 +118,7 @@ export const prepareTransaction = (data: any) => {
// handle type: `json` // handle type: `json`
if (_value && typeof _value === "object" && _value.$type === "json") { if (_value && typeof _value === "object" && _value.$type === "json") {
if (typeof _value.$value === "object") { if (typeof _value.$value === "object") {
options[field] = _value.$value; options[field] = _value.$value as any;
} else { } else {
try { try {
options[field] = JSON.parse(_value.$value); options[field] = JSON.parse(_value.$value);
@@ -137,8 +131,8 @@ export const prepareTransaction = (data: any) => {
} }
} }
// delete unnecessary fields // delete unneccesary fields
if (!options[field]) { if (options[field] === undefined) {
delete options[field]; delete options[field];
} }
}); });
@@ -158,7 +152,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 schema = getTxFields(transactionType) const txFields = getTxFields(transactionType)
if (Account) { if (Account) {
const acc = state.accounts.find(acc => acc.address === Account); const acc = state.accounts.find(acc => acc.address === Account);
@@ -186,8 +180,9 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedTransaction = null; tx.selectedTransaction = null;
} }
if (schema.Destination !== undefined) { if (txFields.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,
@@ -204,14 +199,11 @@ 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 schemaVal = schema[field as keyof TxFields] const origValue = txFields[field as keyof TxFields]
const isXrp = typeof value !== 'object' && schemaVal && typeof schemaVal === 'object' && schemaVal.$type === 'xrp' const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.$type === 'xrp'
if (isXrp) { if (isXrp) {
rest[field] = { rest[field] = {
$type: "xrp", $type: "xrp",
@@ -226,6 +218,7 @@ export const prepareState = (value: string, transactionType?: string) => {
}); });
tx.txFields = rest; tx.txFields = rest;
tx.editorSavedValue = null;
return tx return tx
} }
@@ -251,10 +244,3 @@ 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,7 +53,6 @@ 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",
@@ -366,7 +365,6 @@ 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),
}, },
}); });

View File

@@ -1,21 +0,0 @@
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)" },
});

View File

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

View File

@@ -6,10 +6,4 @@ export const guessZipFileName = (files: File[]) => {
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.') let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
parts = parts.length > 1 ? parts.slice(0, -1) : parts parts = parts.length > 1 ? parts.slice(0, -1) : parts
return parts.join('') return parts.join('')
}
export const capitalize = (value?: string) => {
if (!value) return '';
return value[0].toLocaleUpperCase() + value.slice(1);
} }

View File

@@ -18,18 +18,13 @@ export const tts = {
ttDEPOSIT_PREAUTH: 19, ttDEPOSIT_PREAUTH: 19,
ttTRUST_SET: 20, ttTRUST_SET: 20,
ttACCOUNT_DELETE: 21, ttACCOUNT_DELETE: 21,
ttHOOK_SET: 22, ttHOOK_SET: 22
ttNFTOKEN_MINT: 25,
ttNFTOKEN_BURN: 26,
ttNFTOKEN_CREATE_OFFER: 27,
ttNFTOKEN_CANCEL_OFFER: 28,
ttNFTOKEN_ACCEPT_OFFER: 29
}; };
export type TTS = typeof tts; export type TTS = typeof tts;
const calculateHookOn = (arr: (keyof TTS)[]) => { const calculateHookOn = (arr: (keyof TTS)[]) => {
let start = '0x000000003e3ff5bf'; let start = '0x00000000003ff5bf';
arr.forEach(n => { arr.forEach(n => {
let v = BigInt(start); let v = BigInt(start);
v ^= (BigInt(1) << BigInt(tts[n as keyof TTS])); v ^= (BigInt(1) << BigInt(tts[n as keyof TTS]));

View File

@@ -1,78 +0,0 @@
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;
};

133
yarn.lock
View File

@@ -594,18 +594,6 @@
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"
@@ -647,9 +635,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.6": "@radix-ui/react-dropdown-menu@^0.1.1":
version "0.1.6" version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz#3203229788cd57e552c9f19dcc7008e2b545919c" resolved "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz"
integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg== integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
@@ -1292,6 +1280,14 @@ babel-plugin-macros@^2.6.1:
cosmiconfig "^6.0.0" cosmiconfig "^6.0.0"
resolve "^1.12.0" resolve "^1.12.0"
babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
@@ -1529,11 +1525,6 @@ color-name@~1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
comment-parser@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b"
integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
@@ -1556,6 +1547,11 @@ core-js-pure@^3.20.2:
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz" resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz"
integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ==
core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-util-is@~1.0.0: core-util-is@~1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
@@ -2285,6 +2281,18 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz"
integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
handlebars@^4.7.7:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
source-map "^0.6.1"
wordwrap "^1.0.0"
optionalDependencies:
uglify-js "^3.1.4"
has-bigints@^1.0.1: has-bigints@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz" resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz"
@@ -2953,10 +2961,15 @@ 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.10.3: neo-async@^2.6.0:
version "4.10.3" version "2.6.2"
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.3.tgz#0a952dd5004fd2ac2ba414c990922cf9b33951a3" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-7zc4aXYc/EEln7Pkcsn21V1IevaTZsMLJwapfbnKA4+JY0+jFzWbt5p/ljugesGIrN4VOZhpZIw50EaFZyghJQ== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next-auth@^4.0.0-beta.5:
version "4.2.1"
resolved "https://registry.npmjs.org/next-auth/-/next-auth-4.2.1.tgz"
integrity sha512-XDtt7nqevkNf4EJ2zKAKkI+MFsURf11kx11vPwxrBYA1MHeqWwaWbGOUOI2ekNTvfAg4nTEJJUH3LV2cLrH3Tg==
dependencies: dependencies:
"@babel/runtime" "^7.16.3" "@babel/runtime" "^7.16.3"
"@panva/hkdf" "^1.0.1" "@panva/hkdf" "^1.0.1"
@@ -2968,11 +2981,6 @@ next-auth@^4.10.3:
preact-render-to-string "^5.1.19" preact-render-to-string "^5.1.19"
uuid "^8.3.2" uuid "^8.3.2"
next-plausible@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/next-plausible/-/next-plausible-3.2.0.tgz#d801346253e0c1cf64a02b9fc3a42050455cbc47"
integrity sha512-OlYcLXBG3kKd/fKMpm8SZ5IkUKSFm1/8t7cv6e5bewIqlpdZpdWuSrjbdJpbmutb2KPLXHzilKp09zmDGjy9KQ==
next-themes@^0.1.1: next-themes@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.1.1.tgz#122113a458bf1d1be5ffed66778ab924c106f82a" resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.1.1.tgz#122113a458bf1d1be5ffed66778ab924c106f82a"
@@ -3544,6 +3552,11 @@ reconnecting-websocket@^4.4.0:
resolved "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz" resolved "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz"
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.4: regenerator-runtime@^0.13.4:
version "0.13.9" version "0.13.9"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
@@ -3642,25 +3655,30 @@ ripple-address-codec@^4.1.0, ripple-address-codec@^4.1.1, ripple-address-codec@^
base-x "3.0.9" base-x "3.0.9"
create-hash "^1.1.2" create-hash "^1.1.2"
ripple-address-codec@^4.2.4: ripple-binary-codec@^0.2.4:
version "4.2.4" version "0.2.7"
resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-4.2.4.tgz#a56c2168c8bb81269ea4d15ed96d6824c5a866f8" resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-0.2.7.tgz"
integrity sha512-roAOjKz94+FboTItey1XRh5qynwt4xvfBLvbbcx+FiR94Yw2x3LrKLF2GVCMCSAh5I6PkcpADg6AbYsUbGN3nA== integrity sha512-VD+sHgZK76q3kmO765klFHPDCEveS5SUeg/bUNVpNrj7w2alyDNkbF17XNbAjFv+kSYhfsUudQanoaSs2Y6uzw==
dependencies: dependencies:
base-x "3.0.9" babel-runtime "^6.26.0"
create-hash "^1.1.2" bn.js "^5.1.1"
create-hash "^1.2.0"
decimal.js "^10.2.0"
inherits "^2.0.4"
lodash "^4.17.15"
ripple-address-codec "^4.1.0"
ripple-binary-codec@=1.4.2, ripple-binary-codec@^0.2.4, ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.4.2: ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.3.0:
version "1.4.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3" resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-1.3.2.tgz"
integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w== integrity sha512-8VG1vfb3EM1J7ZdPXo9E57Zv2hF4cxT64gP6rGSQzODVgMjiBCWozhN3729qNTGtHItz0e82Oix8v95vWYBQ3A==
dependencies: dependencies:
assert "^2.0.0" assert "^2.0.0"
big-integer "^1.6.48" big-integer "^1.6.48"
buffer "5.6.0" buffer "5.6.0"
create-hash "^1.2.0" create-hash "^1.2.0"
decimal.js "^10.2.0" decimal.js "^10.2.0"
ripple-address-codec "^4.2.4" ripple-address-codec "^4.2.3"
ripple-bs58@^4.0.0: ripple-bs58@^4.0.0:
version "4.0.1" version "4.0.1"
@@ -3857,6 +3875,11 @@ source-map@^0.5.7:
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
split.js@^1.6.0: split.js@^1.6.0:
version "1.6.5" version "1.6.5"
resolved "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz" resolved "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz"
@@ -4088,6 +4111,11 @@ typescript@4.4.4:
resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz" resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz"
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
uglify-js@^3.1.4:
version "3.16.0"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.0.tgz#b778ba0831ca102c1d8ecbdec2d2bdfcc7353190"
integrity sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==
unbox-primitive@^1.0.1: unbox-primitive@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz"
@@ -4307,6 +4335,11 @@ word-wrap@^1.2.3:
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
wrappy@1: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
@@ -4317,10 +4350,10 @@ ws@^7.2.0:
resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz" resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz"
integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
xrpl-accountlib@^1.5.2: xrpl-accountlib@^1.3.2:
version "1.5.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/xrpl-accountlib/-/xrpl-accountlib-1.5.2.tgz#8f16abe449fd60ba9ed75597f6ce3f0c45dfff43" resolved "https://registry.npmjs.org/xrpl-accountlib/-/xrpl-accountlib-1.3.2.tgz"
integrity sha512-lieY2/5G9DySqdtgQ0AD/aMMG5Sy/MLAmbIsmsCaF06scM5DpR8s4SsEzgHni7dOG68Wjnb2Uz6tf5aV+l4/Kg== integrity sha512-mXwoumGp0xUiZ7Ty/1o4FHVRK4uLnqngxdYmikZs50drMjlgCUP6GXun2Vf4Uus1fnVnxhXIw+E7peH5OjiOJA==
dependencies: dependencies:
assert "^2.0.0" assert "^2.0.0"
bip32 "^2.0.5" bip32 "^2.0.5"
@@ -4329,13 +4362,13 @@ xrpl-accountlib@^1.5.2:
elliptic "6.5.4" elliptic "6.5.4"
hash.js "^1.1.7" hash.js "^1.1.7"
ripple-address-codec "^4.1.0" ripple-address-codec "^4.1.0"
ripple-binary-codec "^1.4.2" ripple-binary-codec "^1.3.0"
ripple-hashes "^0.3.4" ripple-hashes "^0.3.4"
ripple-keypairs "^1.0.3" ripple-keypairs "^1.0.3"
ripple-lib "^1.6.4" ripple-lib "^1.6.4"
ripple-secret-codec "^1.0.2" ripple-secret-codec "^1.0.2"
xrpl-secret-numbers "^0.3.3" xrpl-secret-numbers "^0.3.3"
xrpl-sign-keypairs "^2.1.1" xrpl-sign-keypairs "^2.0.1"
xrpl-client@^1.9.4: xrpl-client@^1.9.4:
version "1.9.4" version "1.9.4"
@@ -4354,13 +4387,13 @@ xrpl-secret-numbers@^0.3.3:
brorand "^1.1.0" brorand "^1.1.0"
ripple-keypairs "^1.0.3" ripple-keypairs "^1.0.3"
xrpl-sign-keypairs@^2.1.1: xrpl-sign-keypairs@^2.0.1:
version "2.1.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/xrpl-sign-keypairs/-/xrpl-sign-keypairs-2.1.1.tgz#2f7f2855799c5d4ba091007963825eef1db21a4e" resolved "https://registry.npmjs.org/xrpl-sign-keypairs/-/xrpl-sign-keypairs-2.0.1.tgz"
integrity sha512-rKQmUCx+x7gjjJ5zv/Z7bOYR+8I36JwUCFlpuD9UzYD4w2msGQDG0rmxVENyZSfThDBVQ1kEArVn6SMDMe9LUQ== integrity sha512-84QbE3trxetaw0hqDADCWMx0HH1VAWnTJp0TGoKTGRf1jzTqjI7eNNNw5lmcay2MH8bW/waNzJIF8vSAJSkVrQ==
dependencies: dependencies:
big-integer latest big-integer latest
ripple-binary-codec "^1.4.2" ripple-binary-codec "^1.3.0"
ripple-bs58check latest ripple-bs58check latest
ripple-hashes latest ripple-hashes latest
ripple-keypairs latest ripple-keypairs latest