Compare commits

..

1 Commits

Author SHA1 Message Date
Vaclav Barta
a82de7087d -O0 -> -O2 2022-06-22 08:47:00 +02:00
45 changed files with 2051 additions and 2433 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
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noopener noreferrer"
> >
{activeAccount?.address} {activeAccount?.address}
</a>
</Text> </Text>
</Flex> </Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}> <Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
@@ -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>
Account name <Text muted>(optional)</Text>
</Label>
<Input <Input
name="name"
type="text"
autoComplete="off"
autoCapitalize="on"
value={name}
onChange={e => setName(e.target.value)}
/>
</Box>
{type === "import" && (
<Box>
<Label>Account secret</Label>
<Input
required
name="secret" name="secret"
type="password" type="password"
autoComplete="new-password" value={value}
value={secret} onChange={(e) => setValue(e.target.value)}
onChange={e => setSecret(e.target.value)}
/> />
</Box> </DialogDescription>
)}
</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,152 +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";
[
{
label: "Show bookmarks",
type: "checkbox",
checked: true,
indicator: "*",
onCheckedChange: () => {},
},
{
type: "radio",
label: "People",
value: "pedro",
onValueChange: () => {},
options: [
{
value: "pedro",
label: "Pedro Duarte",
},
{
value: "colm",
label: "Colm Tuite",
},
],
},
];
export type TextOption = {
type: "text";
label: ReactNode;
onClick?: () => any;
children?: Option[];
};
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 };
type Option = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
WithCommons;
interface IContextMenu {
options?: Option[];
isNested?: boolean;
}
export const ContextMenu: FC<IContextMenu> = ({
children,
options,
isNested,
}) => {
return (
<ContextMenuRoot>
{isNested ? (
<ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
) : (
<ContextMenuTrigger>{children}</ContextMenuTrigger>
)}
{options && (
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
{options.map(({ key, ...option }) => {
if (option.type === "text") {
const { children, label } = 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}>{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

@@ -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,13 +22,10 @@ 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";
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path; const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find(h => currPath?.endsWith(h))) { if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true }); editor.updateOptions({ readOnly: true });
} else { } else {
editor.updateOptions({ readOnly: false }); editor.updateOptions({ readOnly: false });
@@ -42,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-")
@@ -56,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,
@@ -113,33 +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)}
headerExtraValidation={{
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
error:
'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"',
}}
>
{snap.files.map((file, index) => {
return <Tab key={file.name} header={file.name} />;
})}
</Tabs>
);
return ( return (
<Box <Box
css={{ css={{
@@ -152,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,
@@ -188,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,
@@ -212,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) => {
@@ -239,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}
@@ -191,12 +189,12 @@ 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
key={template.id}
as="a" as="a"
href={`/develop/${template.id}`} href={`/develop/${templateFileIds.starter}`}
> >
<ImageWrapper>{template.icon()}</ImageWrapper> <ImageWrapper>
<Heading>{template.name}</Heading> <Starter />
</ImageWrapper>
<Heading>Starter</Heading>
<Text>{template.description}</Text> <Text>
Just a basic starter with essential imports, just
accepts any transaction coming through
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.firewall}`}
css={{ alignItems: "flex-start" }}
>
<ImageWrapper>
<Firewall />
</ImageWrapper>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.notary}`}
>
<ImageWrapper>
<Notary />
</ImageWrapper>
<Heading>Notary</Heading>
<Text>
Collecting signatures for multi-sign transactions
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.carbon}`}
>
<ImageWrapper>
<Carbon />
</ImageWrapper>
<Heading>Carbon</Heading>
<Text>Send a percentage of sum to an address</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.peggy}`}
>
<ImageWrapper>
<Peggy />
</ImageWrapper>
<Heading>Peggy</Heading>
<Text>An oracle based stable coin hook</Text>
</PanelBox> </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,20 +55,7 @@ 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>
@@ -91,57 +69,72 @@ 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);
let _fields = tags.map(tag => ({
name: tag.name,
value: tag.default || "",
type: tag.type,
description: tag.description,
required: !tag.optional,
}));
const fields: Fields = _fields.reduce((acc, field) => {
acc[field.name] = field;
return acc;
}, {} as Fields);
const error = getErrors(content);
if (error) setTemplateError(error.message);
else setTemplateError("");
return fields;
}, [content]);
const runScript = useCallback(() => {
try {
let data: any = {};
Object.keys(fields).forEach(key => {
data[key] = fields[key].value;
}); });
const template = generateHtmlTemplate(content, data); const template = Handlebars.compile(content, { strict: false });
try {
setIframeCode(template); const code = template(fieldsToSend);
setIframeCode(generateHtmlTemplate(code));
state.scriptLogs = [ state.scriptLogs = [
...snap.scriptLogs, ...snap.scriptLogs,
{ type: "success", message: "Started running..." }, { type: "success", message: "Started running..." },
@@ -153,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) => {
@@ -170,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}>
@@ -210,77 +191,65 @@ 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 (
<Box key={name} css={{ width: "100%" }}>
<Label
css={{ display: "flex", justifyContent: "space-between" }}
>
<span>
{description || name} {required && <Text error>*</Text>}
</span>
{isAccountSecret && (
<Text error small css={{ alignSelf: "end" }}>
can access account secret key
</Text>
)}
</Label>
{isAccount ? (
<Select <Select
css={{ mt: "$1" }} css={{ mt: "$1" }}
options={accOptions} options={options}
onChange={(val: any) => { onChange={(val: any) => {
setFields({ setFields({
...fields, ...fields,
[key]: { [key]: {
...fields[key], ...fields[key],
value: val[accountField], value:
fields[key].attach === "account_secret"
? val.secret
: val.address,
}, },
}); });
}} }}
value={accOptions.find( value={options.find(
(acc: any) => acc[accountField] === value (opt) =>
opt.address === fields[key].value ||
opt.secret === fields[key].value
)} )}
/> />
) : ( ) : (
<Input <Input
type={type || "text"} type={fields[key].type || "text"}
value={value} value={
typeof fields[key].value !== "string"
? // @ts-expect-error
fields[key].value.value
: fields[key].value
}
css={{ mt: "$1" }} css={{ mt: "$1" }}
onChange={e => { onChange={(e) => {
setFields({ setFields({
...fields, ...fields,
[key]: { ...fields[key], value: e.target.value }, [key]: { ...fields[key], value: e.target.value },
@@ -289,8 +258,7 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
/> />
)} )}
</Box> </Box>
); ))}
})}
<Flex <Flex
css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }} css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }}
> >
@@ -299,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

@@ -22,12 +22,12 @@ import {
import { TTS, tts } from "../utils/hookOnCalculator"; 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";
const transactionOptions = Object.keys(tts).map(key => ({ const transactionOptions = Object.keys(tts).map((key) => ({
label: key, label: key,
value: key as keyof TTS, value: key as keyof TTS,
})); }));
@@ -56,31 +56,9 @@ export type SetHookData = {
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 compiledFiles = snap.files.filter(file => file.compiledContent); const account = snap.accounts.find((acc) => acc.address === accountAddress);
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false); const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState(
accountOptions.find(acc => acc.value === accountAddress)
);
const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value
);
const getHookNamespace = useCallback(
() =>
activeFile && snap.deployValues[activeFile.name]
? snap.deployValues[activeFile.name].HookNamespace
: activeFile?.name.split(".")[0] || "",
[activeFile, snap.deployValues]
);
const { const {
register, register,
handleSubmit, handleSubmit,
@@ -90,9 +68,10 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
getValues, getValues,
formState: { errors }, formState: { errors },
} = useForm<SetHookData>({ } = useForm<SetHookData>({
defaultValues: (activeFile && snap.deployValues[activeFile.name]) || { defaultValues: {
HookNamespace: activeFile?.name.split(".")[0] || "", HookNamespace:
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"), snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
Invoke: transactionOptions.filter((to) => to.label === "ttPAYMENT"),
}, },
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
@@ -102,16 +81,14 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const [formInitialized, setFormInitialized] = useState(false); const [formInitialized, setFormInitialized] = useState(false);
const [estimateLoading, setEstimateLoading] = useState(false); const [estimateLoading, setEstimateLoading] = useState(false);
const watchedFee = watch("Fee"); const watchedFee = watch("Fee");
// Update value if activeWat changes
// Update value if activeFile changes
useEffect(() => { useEffect(() => {
if (!activeFile) return; setValue(
const defaultValue = getHookNamespace(); "HookNamespace",
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
setValue("HookNamespace", defaultValue); );
setFormInitialized(true); setFormInitialized(true);
}, [setValue, activeFile, snap.deployValues, getHookNamespace]); }, [snap.activeWat, snap.files, setValue]);
useEffect(() => { useEffect(() => {
if ( if (
watchedFee && watchedFee &&
@@ -129,19 +106,19 @@ 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]);
// Calculate initial fee estimate when modal opens // Calcucate initial fee estimate when modal opens
useEffect(() => { useEffect(() => {
if (formInitialized && account) { if (formInitialized && account) {
(async () => { (async () => {
@@ -159,18 +136,14 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [formInitialized]); }, [formInitialized]);
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;
const res = await deployHook(account, data); const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false; if (currAccount) currAccount.isLoading = false;
@@ -190,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
@@ -201,22 +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"
hideSelectedOptions
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}
@@ -233,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" }}>
@@ -295,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();
} }
@@ -327,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 {
@@ -428,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

@@ -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 from "./ContextMenu";
const ErrorText = styled(Text, { const ErrorText = styled(Text, {
color: "$error", color: "$error",
@@ -27,25 +25,19 @@ const ErrorText = styled(Text, {
}); });
interface TabProps { interface TabProps {
header: string; header?: string;
children?: ReactNode; children: ReactNode;
} }
// 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;
onCloseTab?: (index: number, header?: string) => any; onCloseTab?: (index: number, header?: string) => any;
onChangeActive?: (index: number, header?: string) => any; onChangeActive?: (index: number, header?: string) => any;
@@ -54,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,
@@ -63,10 +54,8 @@ export const Tabs = ({
onCreateNewTab, onCreateNewTab,
onCloseTab, onCloseTab,
onChangeActive, onChangeActive,
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);
@@ -94,38 +83,12 @@ export const Tabs = ({
const validateTabname = useCallback( const validateTabname = useCallback(
(tabname: string): { error: string | null } => { (tabname: string): { error: string | null } => {
if (!tabname) {
return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
}
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." };
}
const ext =
(tabname.includes(".") && tabname.split(".").pop()) ||
defaultExtension ||
"";
if (extensionRequired && !ext) {
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 { error: null }; return { error: null };
}, },
[ [tabs]
allowedExtensions,
defaultExtension,
extensionRequired,
headerExtraValidation,
label,
tabs,
]
); );
const handleActiveChange = useCallback( const handleActiveChange = useCallback(
@@ -137,25 +100,29 @@ export const Tabs = ({
); );
const handleCreateTab = useCallback(() => { const handleCreateTab = useCallback(() => {
const chk = validateTabname(tabname); // add default extension in case omitted
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = _tabname + defaultExtension;
}
const chk = validateTabname(_tabname);
if (chk.error) { if (chk.error) {
setNewtabError(`Error: ${chk.error}`); setNewtabError(`Error: ${chk.error}`);
return; return;
} }
let _tabname = tabname;
if (defaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
}
setIsNewtabDialogOpen(false); setIsNewtabDialogOpen(false);
setTabname(""); setTabname("");
onCreateNewTab?.(_tabname); onCreateNewTab?.(_tabname);
// switch to new tab?
handleActiveChange(tabs.length, _tabname); handleActiveChange(tabs.length, _tabname);
}, [ }, [
tabname, tabname,
defaultExtension, defaultExtension,
forceDefaultExtension,
validateTabname, validateTabname,
onCreateNewTab, onCreateNewTab,
handleActiveChange, handleActiveChange,
@@ -169,10 +136,8 @@ export const Tabs = ({
} }
onCloseTab?.(idx, tabs[idx].header); onCloseTab?.(idx, tabs[idx].header);
handleActiveChange(idx, tabs[idx].header);
}, },
[active, handleActiveChange, onCloseTab, tabs] [active, onCloseTab, tabs]
); );
return ( return (
@@ -189,8 +154,8 @@ export const Tabs = ({
}} }}
> >
{tabs.map((tab, idx) => ( {tabs.map((tab, idx) => (
<ContextMenu key={tab.header}>
<Button <Button
key={tab.header}
role="tab" role="tab"
tabIndex={idx} tabIndex={idx}
onClick={() => handleActiveChange(idx, tab.header)} onClick={() => handleActiveChange(idx, tab.header)}
@@ -229,7 +194,6 @@ export const Tabs = ({
</Box> </Box>
)} )}
</Button> </Button>
</ContextMenu>
))} ))}
{onCreateNewTab && ( {onCreateNewTab && (
<Dialog <Dialog
@@ -242,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)}
@@ -288,8 +249,8 @@ export const Tabs = ({
)} )}
</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;
@@ -302,14 +263,11 @@ export const Tabs = ({
<children.type <children.type
key={key} key={key}
{...props} {...props}
style={{ style={{ ...style, display: active !== idx ? "none" : undefined }}
...style,
display: active !== idx ? "none" : undefined,
}}
/> />
); );
}) })
: tabs[active] && ( ) : (
<Fragment key={tabs[active].header || active}> <Fragment key={tabs[active].header || active}>
{tabs[active].children} {tabs[active].children}
</Fragment> </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,
getTxFields,
modifyTransaction, modifyTransaction,
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,6 +34,7 @@ const Transaction: FC<TransactionProps> = ({
txIsDisabled, txIsDisabled,
txIsLoading, txIsLoading,
viewType, viewType,
editorSavedValue,
editorValue, editorValue,
} = txState; } = txState;
@@ -48,7 +46,7 @@ const Transaction: FC<TransactionProps> = ({
); );
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 = modifyTransaction(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,30 +177,31 @@ 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" }}
>
<Editor
className="hooks-editor"
language={"json"} language={"json"}
id={header} path={path}
height="100%" height="100%"
beforeMount={monaco => {
monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme("light", light as any);
}}
value={editorValue} value={editorValue}
onChange={val => setState({ editorValue: val })} onChange={val => setState({ editorValue: val })}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
@@ -200,33 +213,19 @@ export const TxJson: FC<JsonProps> = ({
fontSize: 14, 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()));
}} }}
overlay={ theme={theme === "dark" ? "dark" : "light"}
hasUnsaved ? (
<Flex
row
align="center"
css={{ fontSize: "$xs", color: "$textMuted", ml: "auto" }}
>
<Text muted small>
This file has unsaved changes.
</Text>
<Link
css={{ ml: "$1" }}
onClick={() => saveState(editorValue || "", currTxType)}
>
save
</Link>
<Link css={{ ml: "$1" }} onClick={discardChanges}>
discard
</Link>
</Flex>
) : undefined
}
/> />
{hasUnsaved && (
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
This file has unsaved changes.{" "}
<Link onClick={() => saveState(editorValue, currTxType)}>save</Link>{" "}
<Link onClick={discardChanges}>discard</Link>
</Text>
)}
</Flex>
); );
}; };

View File

@@ -1,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) {
const val = e.target.value
.replaceAll(".", "")
.replaceAll(",", "");
handleSetField(field, val);
} else {
handleSetField(field, e.target.value); 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.1", "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
@@ -128,10 +124,6 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
light: "light", light: "light",
dark: darkTheme.className, dark: darkTheme.className,
}} }}
>
<PlausibleProvider
domain="hooks-builder.xrpl.org"
trackOutboundLinks
> >
<Navigation /> <Navigation />
<Component {...pageProps} /> <Component {...pageProps} />
@@ -150,19 +142,6 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
}} }}
/> />
<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

@@ -6,9 +6,8 @@ import Transaction from "../../components/Transaction";
import state from "../../state"; import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTransaction } 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,20 +66,19 @@ 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 => modifyTransaction(header, {})} onCreateNewTab={(header) => modifyTransaction(header, {})}
onCloseTab={(idx, header) => onCloseTab={(idx, header) =>
header && modifyTransaction(header, undefined) header && modifyTransaction(header, undefined)
} }
@@ -113,7 +95,7 @@ const Test = () => {
</Box> </Box>
</Split> </Split>
</Flex> </Flex>
{hasScripts ? ( {hasScripts && (
<Flex <Flex
as="div" as="div"
css={{ css={{
@@ -122,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,7 +27,7 @@ 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) => {
// Lets limit the number of faucet accounts to 5 for now // Lets limit the number of faucet accounts to 5 for now
if (state.accounts.length > 5) { if (state.accounts.length > 5) {
return toast.error("You can only have maximum 6 accounts"); return toast.error("You can only have maximum 6 accounts");
@@ -52,7 +52,7 @@ export const addFaucetAccount = async (name?: string, showToast: boolean = false
} }
const currNames = state.accounts.map(acc => acc.name); const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({ state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0], name: names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(), xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address, address: json.address,
secret: json.secret, secret: json.secret,

View File

@@ -14,21 +14,19 @@ 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, { const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: {
@@ -42,8 +40,8 @@ export const compileCode = async (activeId: number) => {
{ {
type: "c", type: "c",
options: state.compileOptions.optimizationLevel || '-O2', options: state.compileOptions.optimizationLevel || '-O2',
name: file.name, name: state.files[activeId].name,
src: file.content, src: state.files[activeId].content,
}, },
], ],
}), }),
@@ -51,15 +49,15 @@ export const compileCode = async (activeId: number) => {
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" });
} }
state.logs.push({ state.logs.push({
type: "success", type: "success",
@@ -69,9 +67,8 @@ export const compileCode = async (activeId: number) => {
}); });
// Decode base64 encoded wasm that is coming back from the endpoint // Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output); const bufferData = await decodeBinary(json.output);
file.compiledContent = ref(bufferData); state.files[state.active].compiledContent = ref(bufferData);
file.lastCompiled = new Date(); state.files[state.active].lastCompiled = new Date();
file.compiledValueSnapshot = file.content
// Import wabt from and create human readable version of wasm file and // Import wabt from and create human readable version of wasm file and
// put it into state // put it into state
import("wabt").then((wabt) => { import("wabt").then((wabt) => {
@@ -87,23 +84,10 @@ export const compileCode = async (activeId: number) => {
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
if (err instanceof Array && typeof err[0] === 'string') {
err.forEach(message => {
state.logs.push({ state.logs.push({
type: "error", type: "error",
message, message: "Error occured while compiling!",
}); });
})
}
else {
state.logs.push({
type: "error",
message: "Something went wrong, check your connection try again later!",
});
}
state.compiling = false; state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true
} }
}; };

View File

@@ -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;
const updateActiveWat = () => { } else {
const filename = state.files[state.active]?.name state.activeWat = active;
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,14 +36,15 @@ 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"
@@ -90,7 +92,7 @@ export const modifyTransaction = (
} }
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];
@@ -116,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);
@@ -129,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];
} }
}); });
@@ -150,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);
@@ -178,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,
@@ -196,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",
@@ -218,6 +218,7 @@ export const prepareState = (value: string, transactionType?: string) => {
}); });
tx.txFields = rest; tx.txFields = rest;
tx.editorSavedValue = null;
return tx return tx
} }
@@ -243,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

@@ -7,9 +7,3 @@ export const guessZipFileName = (files: File[]) => {
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]));

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.1: neo-async@^2.6.0:
version "4.10.1" version "2.6.2"
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.1.tgz#33b29265d12287bb2f6d267c8d415a407c27f0e9" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-F00vtwBdyMIIJ8IORHOAOHjVGTDEhhm9+HpB2BQ8r40WtGxqToWWLN7Z+2ZW/z2RFlo3zhcuAtUCPUzVJxtZwQ== 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.1:
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