Compare commits

...

50 Commits

Author SHA1 Message Date
Valtteri Karesto
491e10920b Updated label 2022-05-10 13:25:41 +03:00
Valtteri Karesto
65bb209713 Fixed wrong key 2022-05-10 11:53:31 +03:00
Valtteri Karesto
c07e70acc9 Add popover descriptions 2022-05-10 11:52:03 +03:00
Valtteri Karesto
8fd7f8ecad Change state key 2022-05-10 11:51:49 +03:00
Valtteri Karesto
2bb3c646db Add compile logic to ui 2022-05-09 14:18:32 +03:00
Valtteri Karesto
87f10a11b0 Add switch component and bg color to popover 2022-05-09 14:18:23 +03:00
Valtteri Karesto
949fb45ae2 Add colors 2022-05-09 14:18:02 +03:00
Valtteri Karesto
ea52f014dd Add radix switch 2022-05-09 14:17:55 +03:00
Valtteri Karesto
77eab8d88d Merge pull request #182 from eqlabs/fix/save-before-sync
"Save" files before syncing to github
2022-05-09 12:56:37 +03:00
Vaclav Barta
4ca8f5f236 Merge pull request #184 from eqlabs/feature/hook-doc-upd
documentation for new checks
2022-05-09 08:36:07 +02:00
Vaclav Barta
814b074cc0 added hooks-control-string-arg, hooks-release-define and hooks-skip-hash-buf-len docs 2022-05-04 14:22:25 +02:00
Valtteri Karesto
822f7a30f5 "Save" files before syncing to github 2022-05-03 16:33:17 +03:00
Valtteri Karesto
1d66137c23 Merge pull request #181 from eqlabs/feat/add-optimization-settings
Feat/add optimization settings
2022-05-03 15:30:51 +03:00
Valtteri Karesto
4c42e75686 Remove example 2022-05-03 14:46:42 +03:00
Valtteri Karesto
501b7fefec Removed unnecessary setting 2022-05-03 14:17:36 +03:00
Valtteri Karesto
aa7e1517a2 Removed unused import 2022-05-03 14:07:43 +03:00
Valtteri Karesto
e33093f160 Remove console.log 2022-05-03 14:04:18 +03:00
Valtteri Karesto
923b689c98 Add compile options to ui 2022-05-03 14:03:14 +03:00
Valtteri Karesto
246e7f137f Add compile options to compile function 2022-05-03 14:03:02 +03:00
Valtteri Karesto
5defd12a11 Add popover component 2022-05-03 14:02:43 +03:00
Valtteri Karesto
abb7c2bb28 Add compileoptions to global state 2022-05-03 14:02:26 +03:00
Valtteri Karesto
12013907f8 Added radix popover 2022-05-03 14:02:16 +03:00
Valtteri Karesto
ec75fff74b Merge pull request #179 from eqlabs/fix/issue-175
Initial fix for issue #175
2022-05-03 11:20:19 +03:00
Valtteri Karesto
7c1068449f Initial fix for issue #175 2022-04-28 11:46:24 +03:00
Valtteri Karesto
b66d2a09a0 Merge pull request #178 from eqlabs/fix/issue-177
Fixes issue #177
2022-04-28 10:53:26 +03:00
Valtteri Karesto
54265e024c Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into fix/issue-177 2022-04-27 15:38:53 +03:00
Valtteri Karesto
20cb66ba18 Fixes issue #177 2022-04-27 15:33:42 +03:00
Vaclav Barta
b7d62dda83 Merge pull request #174 from eqlabs/feat/req-bin-hook-params
requiring user-quoted Hook Parameter values
2022-04-22 13:25:48 +02:00
Vaclav Barta
c690334f92 requiring user-quoted Hook Parameter values 2022-04-21 09:02:59 +02:00
muzamil
587f09ec00 Merge pull request #168 from eqlabs/feat/improved-logs
Background logs.
2022-04-20 16:45:14 +05:30
muzam1l
9296ea1acc remove a console.log 2022-04-20 16:44:49 +05:30
muzam1l
582fb17c94 Link ledger index to explorer 2022-04-20 13:45:17 +05:30
muzam1l
aff0142870 Debug history log only after clear timestamp 2022-04-19 20:51:11 +05:30
muzamil
df51d87cb2 Merge branch 'main' into feat/improved-logs 2022-04-19 20:43:05 +05:30
muzam1l
6a46f5f173 Merge branch 'main' into feat/improved-logs 2022-04-19 18:52:13 +05:30
muzamil
9e25cefef9 Merge pull request #162 from eqlabs/feat/transaction-persistence
Persisted transactions and debug stream state.
2022-04-19 18:42:46 +05:30
muzam1l
95966fa514 Merge branch 'main' into feat/transaction-persistence 2022-04-19 18:38:56 +05:30
muzam1l
dfe5589074 fix 2022-04-14 17:05:36 +05:30
muzam1l
cdc50da840 don't show legacy logs in debug stream 2022-04-14 16:31:19 +05:30
muzam1l
4893b41936 revert to browser time parsing only 2022-04-14 15:02:00 +05:30
muzam1l
bf21fe36c3 timestamp and log fixes 2022-04-13 21:44:24 +05:30
muzam1l
a33a3eb6e2 Background logs 2022-04-13 16:59:02 +05:30
muzam1l
46e6927c68 Fix debug stream default account being enforcive 2022-04-08 16:33:46 +05:30
muzam1l
de95a82c5a Fix tx fields not updating 2022-04-08 16:25:03 +05:30
muzam1l
3b66d64c14 Fix debug stream default account when changing tx tabs 2022-04-08 16:03:37 +05:30
muzam1l
6e90a4c3d7 some refactoring 2022-04-08 15:17:54 +05:30
muzam1l
2287e7babb Added Label comp and minor style fixes 2022-04-08 14:41:08 +05:30
muzam1l
c219f7ea00 Persisted transaction tabs state 2022-04-07 16:10:16 +05:30
muzam1l
04e2274dbf some refactoring 2022-04-06 14:42:39 +05:30
muzam1l
842b8a5226 Set debug stream default account from transaction account 2022-04-04 21:29:08 +05:30
32 changed files with 13866 additions and 1111 deletions

View File

@@ -18,7 +18,7 @@ import {
DialogTrigger,
} from "./Dialog";
import { css } from "../stitches.config";
import { Input } from "./Input";
import { Input, Label } from "./Input";
import truncate from "../utils/truncate";
const labelStyle = css({
@@ -491,7 +491,7 @@ const ImportAccountDialog = () => {
<DialogContent>
<DialogTitle>Import account</DialogTitle>
<DialogDescription>
<label>Add account secret</label>
<Label>Add account secret</Label>
<Input
name="secret"
type="password"

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect } from "react";
import { proxy, ref, useSnapshot } from "valtio";
import { Select } from ".";
import state, { ILog } from "../state";
import state, { ILog, transactionsState } from "../state";
import { extractJSON } from "../utils/json";
import LogBox from "./LogBox";
@@ -10,17 +10,26 @@ interface ISelect<T = string> {
value: T;
}
const streamState = proxy({
export interface IStreamState {
selectedAccount: ISelect | null;
status: "idle" | "opened" | "closed";
statusChangeTimestamp?: number;
logs: ILog[];
socket?: WebSocket;
}
export const streamState = proxy<IStreamState>({
selectedAccount: null as ISelect | null,
status: "idle",
logs: [] as ILog[],
socket: undefined as WebSocket | undefined,
});
const DebugStream = () => {
const { selectedAccount, logs, socket } = useSnapshot(streamState);
const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
const { accounts } = useSnapshot(state);
const accountOptions = accounts.map((acc) => ({
const accountOptions = accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
@@ -33,39 +42,12 @@ const DebugStream = () => {
options={accountOptions}
hideSelectedOptions
value={selectedAccount}
onChange={(acc) => (streamState.selectedAccount = acc as any)}
onChange={acc => (streamState.selectedAccount = acc as any)}
css={{ width: "100%" }}
/>
</>
);
const prepareLog = useCallback((str: any): ILog => {
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))\ ?([\s\S]*)/m);
const [_, tm, msg] = match || [];
const extracted = extractJSON(msg);
const timestamp = isNaN(Date.parse(tm || ""))
? tm
: new Date(tm).toLocaleTimeString();
const message = !extracted
? msg
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
const jsonData = extracted
? JSON.stringify(extracted.result, null, 2)
: undefined;
return {
type: "log",
message,
timestamp,
jsonData,
defaultCollapsed: true,
};
}, []);
useEffect(() => {
const account = selectedAccount?.value;
if (account && (!socket || !socket.url.endsWith(account))) {
@@ -81,6 +63,50 @@ const DebugStream = () => {
}
}, [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;
@@ -88,37 +114,27 @@ const DebugStream = () => {
const onOpen = () => {
streamState.logs = [];
streamState.logs.push({
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account}`, {
type: "success",
message: `Debug stream opened for account ${account}`,
});
};
const onError = () => {
streamState.logs.push({
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
message: "Something went wrong! Check your connection and try again.",
});
};
const onClose = (e: CloseEvent) => {
streamState.logs.push({
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
message: `Connection was closed. [code: ${e.code}]`,
});
streamState.selectedAccount = null;
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
if (!event.data) return;
const log = prepareLog(event.data);
// Filter out account_info and account_objects requests
try {
const parsed = JSON.parse(log.jsonData);
if (parsed?.id?._Request?.includes("hooks-builder-req")) {
return;
}
} catch (err) {
// Lets just skip if we cannot parse the message
}
return streamState.logs.push(log);
pushLog(event.data);
};
socket.addEventListener("open", onOpen);
@@ -132,16 +148,70 @@ const DebugStream = () => {
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
};
}, [prepareLog, selectedAccount?.value, socket]);
}, [selectedAccount?.value, socket]);
useEffect(() => {
const account = transactionsState.transactions.find(
tx => tx.header === activeTxTab
)?.state.selectedAccount;
if (account && account.value !== streamState.selectedAccount?.value)
streamState.selectedAccount = account;
}, [activeTxTab]);
const clearLog = () => {
streamState.logs = [];
streamState.statusChangeTimestamp = Date.now();
};
return (
<LogBox
enhanced
renderNav={renderNav}
title="Debug stream"
logs={logs}
clearLog={() => (streamState.logs = [])}
clearLog={clearLog}
/>
);
};
export default DebugStream;
export const pushLog = (
str: any,
opts: Partial<Pick<ILog, "type">> = {}
): ILog | undefined => {
if (!str) return;
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m);
const [_, tm, msg] = match || [];
const timestamp = Date.parse(tm || "") || undefined;
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString();
const extracted = extractJSON(msg);
const message = !extracted
? msg
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
const jsonData = extracted
? JSON.stringify(extracted.result, null, 2)
: undefined;
if (extracted?.result?.id?._Request?.includes("hooks-builder-req")) {
return;
}
const { type = "log" } = opts;
const log: ILog = {
type,
message,
timestring,
jsonData,
defaultCollapsed: true,
};
if (log) streamState.logs.push(log);
return log;
};

View File

@@ -47,7 +47,7 @@ import {
} from "./Dialog";
import Flex from "./Flex";
import Stack from "./Stack";
import Input from "./Input";
import { Input, Label } from "./Input";
import Text from "./Text";
import Tooltip from "./Tooltip";
import {
@@ -222,7 +222,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<label>Filename</label>
<Label>Filename</Label>
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
@@ -524,7 +524,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<DialogContent>
<DialogTitle>Editor settings</DialogTitle>
<DialogDescription>
<label>Tab size</label>
<Label>Tab size</Label>
<Input
type="number"
min="1"

View File

@@ -5,7 +5,6 @@ import type monaco from "monaco-editor";
import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import uniqBy from "lodash.uniqby";
import Box from "./Box";
import Container from "./Container";
@@ -45,18 +44,15 @@ const setMarkers = (monacoE: typeof monaco) => {
// Get all the markers that are active at the moment,
// Also if same error is there twice, we can show the content
// only once (that's why we're using uniqBy)
const markers = uniqBy(
monacoE.editor
.getModelMarkers({})
// Filter out the markers that are hooks specific
.filter(
(marker) =>
typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-")
),
"code"
);
const markers = monacoE.editor
.getModelMarkers({})
// Filter out the markers that are hooks specific
.filter(
(marker) =>
typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-")
);
// Get the active model (aka active file you're editing)
// const model = monacoE.editor?.getModel(

View File

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

View File

@@ -147,7 +147,7 @@ const LogBox: FC<ILogBox> = ({
export const Log: FC<ILog> = ({
type,
timestamp: timestamp,
timestring,
message: _message,
link,
linkText,
@@ -186,8 +186,17 @@ export const Log: FC<ILog> = ({
},
[accounts]
);
_message = _message.trim().replace(/\n /gi, "\n");
const message = enrichAccounts(_message);
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 (
@@ -197,9 +206,9 @@ export const Log: FC<ILog> = ({
activeAccountAddress={dialogAccount}
/>
<LogText variant={type}>
{timestamp && (
{timestring && (
<Text muted monospace>
{timestamp}{" "}
{timestring}{" "}
</Text>
)}
<Pre>{message} </Pre>

109
components/Popover.tsx Normal file
View File

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

View File

@@ -52,6 +52,7 @@ const Select = forwardRef<any, Props>((props, ref) => {
control: (provided, state) => {
return {
...provided,
minHeight: 0,
border: "0px",
backgroundColor: colors.mauve4,
boxShadow: `0 0 0 1px ${
@@ -118,32 +119,6 @@ const Select = forwardRef<any, Props>((props, ref) => {
};
},
}}
// theme={(theme) => ({
// ...theme,
// spacing: {
// ...theme.spacing,
// controlHeight: 30,
// },
// colors: {
// primary: colors.selected,
// primary25: colors.active,
// primary50: colors.primary,
// primary75: colors.primary,
// danger: colors.primary,
// dangerLight: colors.primary,
// neutral0: colors.background,
// neutral5: colors.primary,
// neutral10: colors.primary,
// neutral20: colors.outline,
// neutral30: colors.primary,
// neutral40: colors.primary,
// neutral50: colors.placeholder,
// neutral60: colors.primary,
// neutral70: colors.primary,
// neutral80: colors.searchText,
// neutral90: colors.primary,
// },
// })}
{...props}
/>
);

View File

@@ -11,7 +11,7 @@ import {
DialogClose,
DialogTrigger,
} from "./Dialog";
import { Input } from "./Input";
import { Input, Label } from "./Input";
import {
Controller,
SubmitHandler,
@@ -60,16 +60,25 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
handleSubmit,
control,
watch,
setValue,
formState: { errors },
} = useForm<SetHookData>({
defaultValues: {
HookNamespace: snap.files?.[snap.active]?.name?.split(".")?.[0] || "",
HookNamespace: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "HookParameters", // unique name for your Field Array
});
// Update value if activeWat changes
useEffect(() => {
setValue(
"HookNamespace",
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
);
}, [snap.activeWat, snap.files, setValue]);
// const {
// fields: grantFields,
// append: grantAppend,
@@ -132,7 +141,7 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
<DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}>
<Box css={{ width: "100%" }}>
<label>Invoke on transactions</label>
<Label>Invoke on transactions</Label>
<Controller
name="Invoke"
control={control}
@@ -151,12 +160,12 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
/>
</Box>
<Box css={{ width: "100%" }}>
<label>Hook Namespace Seed</label>
<Label>Hook Namespace Seed</Label>
<Input
{...register("HookNamespace", { required: true })}
autoComplete={"off"}
defaultValue={
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
}
/>
{errors.HookNamespace?.type === "required" && (
@@ -165,14 +174,14 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
</Box>
)}
<Box css={{ mt: "$3" }}>
<label>Hook Namespace (sha256)</label>
<Label>Hook Namespace (sha256)</Label>
<Input readOnly value={hashedNamespace} />
</Box>
</Box>
<Box css={{ width: "100%" }}>
<label style={{ marginBottom: "10px", display: "block" }}>
<Label style={{ marginBottom: "10px", display: "block" }}>
Hook parameters
</label>
</Label>
<Stack>
{fields.map((field, index) => (
<Stack key={field.id}>
@@ -184,7 +193,7 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
)}
/>
<Input
placeholder="Parameter value"
placeholder="Value (hex-quoted)"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`
)}

32
components/Switch.tsx Normal file
View File

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

View File

@@ -6,7 +6,7 @@ import React, {
useCallback,
} from "react";
import type { ReactNode, ReactElement } from "react";
import { Box, Button, Flex, Input, Stack, Text } from ".";
import { Box, Button, Flex, Input, Label, Stack, Text } from ".";
import {
Dialog,
DialogTrigger,
@@ -29,7 +29,7 @@ interface TabProps {
children: ReactNode;
}
// TODO customise strings shown
// TODO customise messages shown
interface Props {
activeIndex?: number;
activeHeader?: string;
@@ -40,6 +40,7 @@ interface Props {
forceDefaultExtension?: boolean;
onCreateNewTab?: (name: string) => any;
onCloseTab?: (index: number, header?: string) => any;
onChangeActive?: (index: number, header?: string) => any;
}
export const Tab = (props: TabProps) => null;
@@ -52,11 +53,12 @@ export const Tabs = ({
keepAllAlive = false,
onCreateNewTab,
onCloseTab,
onChangeActive,
defaultExtension = "",
forceDefaultExtension,
}: Props) => {
const [active, setActive] = useState(activeIndex || 0);
const tabs: TabProps[] = children.map((elem) => elem.props);
const tabs: TabProps[] = children.map(elem => elem.props);
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
const [tabname, setTabname] = useState("");
@@ -68,8 +70,9 @@ export const Tabs = ({
useEffect(() => {
if (activeHeader) {
const idx = tabs.findIndex((tab) => tab.header === activeHeader);
setActive(idx);
const idx = tabs.findIndex(tab => tab.header === activeHeader);
if (idx !== -1) setActive(idx);
else setActive(0);
}
}, [activeHeader, tabs]);
@@ -80,7 +83,7 @@ export const Tabs = ({
const validateTabname = useCallback(
(tabname: string): { error: string | null } => {
if (tabs.find((tab) => tab.header === tabname)) {
if (tabs.find(tab => tab.header === tabname)) {
return { error: "Name already exists." };
}
return { error: null };
@@ -88,6 +91,14 @@ export const Tabs = ({
[tabs]
);
const handleActiveChange = useCallback(
(idx: number, header?: string) => {
setActive(idx);
onChangeActive?.(idx, header);
},
[onChangeActive]
);
const handleCreateTab = useCallback(() => {
// add default extension in case omitted
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
@@ -103,11 +114,20 @@ export const Tabs = ({
setIsNewtabDialogOpen(false);
setTabname("");
// switch to new tab?
setActive(tabs.length);
onCreateNewTab?.(_tabname);
}, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
// switch to new tab?
handleActiveChange(tabs.length, _tabname);
}, [
tabname,
defaultExtension,
forceDefaultExtension,
validateTabname,
onCreateNewTab,
handleActiveChange,
tabs.length,
]);
const handleCloseTab = useCallback(
(idx: number) => {
@@ -128,7 +148,7 @@ export const Tabs = ({
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
marginBottom: "$2",
width: "100%",
overflow: "auto",
}}
@@ -138,8 +158,8 @@ export const Tabs = ({
key={tab.header}
role="tab"
tabIndex={idx}
onClick={() => setActive(idx)}
onKeyPress={() => setActive(idx)}
onClick={() => handleActiveChange(idx, tab.header)}
onKeyPress={() => handleActiveChange(idx, tab.header)}
outline={active !== idx}
size="sm"
css={{
@@ -192,11 +212,11 @@ export const Tabs = ({
<DialogContent>
<DialogTitle>Create new tab</DialogTitle>
<DialogDescription>
<label>Tabname</label>
<Label>Tabname</Label>
<Input
value={tabname}
onChange={(e) => setTabname(e.target.value)}
onKeyPress={(e) => {
onChange={e => setTabname(e.target.value)}
onKeyPress={e => {
if (e.key === "Enter") {
handleCreateTab();
}

View File

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

377
components/Transaction.tsx Normal file
View File

@@ -0,0 +1,377 @@
import { Play } from "phosphor-react";
import { FC, useCallback, useEffect } from "react";
import { useSnapshot } from "valtio";
import transactionsData from "../content/transactions.json";
import state, { modifyTransaction } from "../state";
import { sendTransaction } from "../state/actions";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
import { streamState } from "./DebugStream";
import Flex from "./Flex";
import Input from "./Input";
import Text from "./Text";
import Select from "./Select";
type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
type SelectOption = {
value: string;
label: string;
};
export interface TransactionState {
selectedTransaction: SelectOption | null;
selectedAccount: SelectOption | null;
selectedDestAccount: SelectOption | null;
txIsLoading: boolean;
txIsDisabled: boolean;
txFields: TxFields;
}
export interface TransactionProps {
header: string;
state: TransactionState;
}
const Transaction: FC<TransactionProps> = ({
header,
state: {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
txIsDisabled,
txIsLoading,
},
...props
}) => {
const { accounts } = useSnapshot(state);
const setState = useCallback(
(pTx?: Partial<TransactionState>) => {
modifyTransaction(header, pTx);
},
[header]
);
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const destAccountOptions: SelectOption[] = accounts
.map(acc => ({
label: acc.name,
value: acc.address,
}))
.filter(acc => acc.value !== selectedAccount?.value);
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = accounts.find(
acc => acc.address === selectedAccount?.value
);
if (!account || !transactionType || txIsLoading) {
setState({ txIsDisabled: true });
} else {
setState({ txIsDisabled: false });
}
}, [txIsLoading, selectedTransaction, selectedAccount, accounts, setState]);
useEffect(() => {
let _txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === selectedTransaction?.value
);
if (!_txFields) return setState({ txFields: {} });
_txFields = { ..._txFields } as TxFields;
if (!_txFields.Destination) setState({ selectedDestAccount: null });
// @ts-ignore
delete _txFields.TransactionType;
// @ts-ignore
delete _txFields.Account;
// @ts-ignore
delete _txFields.Sequence;
setState({ txFields: _txFields });
}, [setState, selectedTransaction]);
const submitTest = useCallback(async () => {
const account = accounts.find(
acc => acc.address === selectedAccount?.value
);
const TransactionType = selectedTransaction?.value;
if (!account || !TransactionType || txIsDisabled) return;
setState({ txIsLoading: true });
// setTxIsError(null)
try {
let options = { ...txFields };
options.Destination = selectedDestAccount?.value;
(Object.keys(options) as (keyof TxFields)[]).forEach(field => {
let _value = options[field];
// convert currency
if (typeof _value === "object" && _value.type === "currency") {
if (+_value.value) {
options[field] = (+_value.value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (typeof _value === "object" && _value.type === "json") {
if (typeof _value.value === "object") {
options[field] = _value.value as any;
} else {
try {
options[field] = JSON.parse(_value.value);
} catch (error) {
const message = `Input error for json field '${field}': ${
error instanceof Error ? error.message : ""
}`;
throw Error(message);
}
}
}
// delete unneccesary fields
if (!options[field]) {
delete options[field];
}
});
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
await sendTransaction(
account,
{
TransactionType,
...options,
},
{ logPrefix }
);
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({ type: "error", message: error.message });
}
}
setState({ txIsLoading: false });
}, [
header,
setState,
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
accounts,
txFields,
txIsDisabled,
]);
const resetState = useCallback(() => {
setState({});
}, [setState]);
const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc });
streamState.selectedAccount = acc;
};
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(
k => !usualFields.includes(k)
) as OtherFields;
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={(tx: any) => setState({ selectedTransaction: tx })}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/>
</Flex>
{txFields.Amount !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={e =>
setState({
txFields: {
...txFields,
Amount: { type: "currency", value: e.target.value },
},
})
}
css={{ width: "70%", flex: "inherit" }}
/>
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/>
</Flex>
)}
{otherFields.map(field => {
let _value = txFields[field];
let value = typeof _value === "object" ? _value.value : _value;
value =
typeof value === "object"
? JSON.stringify(value)
: value?.toLocaleString();
let isCurrency =
typeof _value === "object" && _value.type === "currency";
return (
<Flex
key={field}
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isCurrency ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e =>
setState({
txFields: {
...txFields,
[field]:
typeof _value === "object"
? { ..._value, value: e.target.value }
: e.target.value,
},
})
}
css={{ width: "70%", flex: "inherit" }}
/>
</Flex>
);
})}
</Flex>
</Container>
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
mb: "$1"
}}
>
<Button outline>VIEW AS JSON</Button>
<Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
);
};
export default Transaction;

View File

@@ -4,7 +4,7 @@ export { default as Container } from "./Container";
export { default as Heading } from "./Heading";
export { default as Stack } from "./Stack";
export { default as Text } from "./Text";
export { default as Input } from "./Input";
export { default as Input, Label } from "./Input";
export { default as Select } from "./Select";
export * from "./Tabs";
export * from "./AlertDialog";

12057
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,9 @@
"@radix-ui/react-dialog": "^0.1.1",
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6",
"@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.6-0",
"base64-js": "^1.5.1",

18
pages/api/proxy.ts Normal file
View File

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

View File

@@ -1,15 +1,19 @@
import { Label } from "@radix-ui/react-label";
import { Switch, SwitchThumb } from "../../components/Switch";
import type { NextPage } from "next";
import dynamic from "next/dynamic";
import { Play } from "phosphor-react";
import { Gear, Play } from "phosphor-react";
import Hotkeys from "react-hot-keys";
import Split from "react-split";
import { useSnapshot } from "valtio";
import { ButtonGroup, Flex } from "../../components";
import Box from "../../components/Box";
import Button from "../../components/Button";
import Popover from "../../components/Popover";
import state from "../../state";
import { compileCode } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { styled } from "../../stitches.config";
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
ssr: false,
@@ -19,6 +23,181 @@ const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const OptimizationText = () => (
<span>
Specify which optimization level to use for compiling. For example -O0 means
no optimization: this level compiles the fastest and generates the most
debuggable code. -O2 means moderate level of optimization which enables most
optimizations. Read more about the options from{" "}
<a
className="link"
rel="noopener noreferrer"
target="_blank"
href="https://clang.llvm.org/docs/CommandGuide/clang.html#cmdoption-o0"
>
clang documentation
</a>
.
</span>
);
const StyledOptimizationText = styled(OptimizationText, {
color: "$mauve12 !important",
fontSize: "200px",
"span a.link": {
color: "red",
},
});
const CompilerSettings = () => {
const snap = useSnapshot(state);
return (
<Flex css={{ minWidth: 200, flexDirection: "column", gap: "$5" }}>
<Box>
<Label
style={{
flexDirection: "row",
display: "flex",
}}
>
Optimization level{" "}
<Popover
css={{
maxWidth: "240px",
lineHeight: "1.3",
a: {
color: "$purple11",
},
".dark &": {
backgroundColor: "$black !important",
".arrow": {
fill: "$colors$black",
},
},
}}
content={<StyledOptimizationText />}
>
<Flex
css={{
position: "relative",
top: "-1px",
ml: "$1",
backgroundColor: "$mauve8",
borderRadius: "$full",
cursor: "pointer",
width: "16px",
height: "16px",
alignItems: "center",
justifyContent: "center",
}}
>
?
</Flex>
</Popover>
</Label>
<ButtonGroup css={{ mt: "$2", fontFamily: "$monospace" }}>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O0"}
onClick={() => (state.compileOptions.optimizationLevel = "-O0")}
>
-O0
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O1"}
onClick={() => (state.compileOptions.optimizationLevel = "-O1")}
>
-O1
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O2"}
onClick={() => (state.compileOptions.optimizationLevel = "-O2")}
>
-O2
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O3"}
onClick={() => (state.compileOptions.optimizationLevel = "-O3")}
>
-O3
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-O4"}
onClick={() => (state.compileOptions.optimizationLevel = "-O4")}
>
-O4
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions.optimizationLevel !== "-Os"}
onClick={() => (state.compileOptions.optimizationLevel = "-Os")}
>
-Os
</Button>
</ButtonGroup>
</Box>
<Box css={{ flexDirection: "column" }}>
<Label
style={{
flexDirection: "row",
display: "flex",
}}
>
Clean WASM{" "}
<Popover
css={{
maxWidth: "240px",
lineHeight: "1.3",
a: {
color: "$purple11",
},
".dark &": {
backgroundColor: "$black !important",
".arrow": {
fill: "$colors$black",
},
},
}}
content="Cleaner removes unwanted compiler-provided exports and functions from a wasm binary to make it (more) suitable for being used as a Hook"
>
<Flex
css={{
position: "relative",
top: "-1px",
mx: "$1",
backgroundColor: "$mauve8",
borderRadius: "$full",
cursor: "pointer",
width: "16px",
height: "16px",
alignItems: "center",
justifyContent: "center",
}}
>
?
</Flex>
</Popover>
</Label>
<Switch
css={{ mt: "$2" }}
checked={snap.compileOptions.strip}
onCheckedChange={(checked) => {
state.compileOptions.strip = checked;
}}
>
<SwitchThumb />
</Switch>
</Box>
</Flex>
);
};
const Home: NextPage = () => {
const snap = useSnapshot(state);
@@ -42,12 +221,7 @@ const Home: NextPage = () => {
!snap.compiling && snap.files.length && compileCode(snap.active)
}
>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
<Flex
css={{
position: "absolute",
bottom: "$4",
@@ -55,11 +229,25 @@ const Home: NextPage = () => {
alignItems: "center",
display: "flex",
cursor: "pointer",
gap: "$2",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
<Popover content={<CompilerSettings />}>
<Button variant="primary" css={{ px: "10px" }}>
<Gear size="16px" />
</Button>
</Popover>
</Flex>
</Hotkeys>
)}
</main>

View File

@@ -1,23 +1,11 @@
import dynamic from "next/dynamic";
import { Play } from "phosphor-react";
import { FC, useCallback, useEffect, useState } from "react";
import Split from "react-split";
import { useSnapshot } from "valtio";
import {
Box,
Button,
Container,
Flex,
Input,
Select,
Tab,
Tabs,
Text,
} from "../../components";
import transactionsData from "../../content/transactions.json";
import { Box, Container, Flex, Tab, Tabs } from "../../components";
import Transaction from "../../components/Transaction";
import state from "../../state";
import { sendTransaction } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTransaction } from "../../state";
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false,
@@ -30,344 +18,10 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
ssr: false,
});
// type SelectOption<T> = { value: T, label: string };
type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
interface Props {
header?: string;
}
const Transaction: FC<Props> = ({ header, ...props }) => {
const snap = useSnapshot(state);
const transactionsOptions = transactionsData.map((tx) => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const [selectedTransaction, setSelectedTransaction] = useState<
typeof transactionsOptions[0] | null
>(null);
const accountOptions = snap.accounts.map((acc) => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState<
typeof accountOptions[0] | null
>(null);
const destAccountOptions = snap.accounts
.map((acc) => ({
label: acc.name,
value: acc.address,
}))
.filter((acc) => acc.value !== selectedAccount?.value);
const [selectedDestAccount, setSelectedDestAccount] = useState<
typeof destAccountOptions[0] | null
>(null);
const [txIsLoading, setTxIsLoading] = useState(false);
const [txIsDisabled, setTxIsDisabled] = useState(false);
const [txFields, setTxFields] = useState<TxFields>({});
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = snap.accounts.find(
(acc) => acc.address === selectedAccount?.value
);
if (!account || !transactionType || txIsLoading) {
setTxIsDisabled(true);
} else {
setTxIsDisabled(false);
}
}, [txIsLoading, selectedTransaction, selectedAccount, snap.accounts]);
useEffect(() => {
let _txFields: TxFields | undefined = transactionsData.find(
(tx) => tx.TransactionType === selectedTransaction?.value
);
if (!_txFields) return setTxFields({});
_txFields = { ..._txFields } as TxFields;
setSelectedDestAccount(null);
// @ts-ignore
delete _txFields.TransactionType;
// @ts-ignore
delete _txFields.Account;
// @ts-ignore
delete _txFields.Sequence;
setTxFields(_txFields);
}, [selectedTransaction, setSelectedDestAccount]);
const submitTest = useCallback(async () => {
const account = snap.accounts.find(
(acc) => acc.address === selectedAccount?.value
);
const TransactionType = selectedTransaction?.value;
if (!account || !TransactionType || txIsDisabled) return;
setTxIsLoading(true);
// setTxIsError(null)
try {
let options = { ...txFields };
options.Destination = selectedDestAccount?.value;
(Object.keys(options) as (keyof TxFields)[]).forEach((field) => {
let _value = options[field];
// convert currency
if (typeof _value === "object" && _value.type === "currency") {
if (+_value.value) {
options[field] = (+_value.value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (typeof _value === "object" && _value.type === "json") {
if (typeof _value.value === "object") {
options[field] = _value.value as any;
} else {
try {
options[field] = JSON.parse(_value.value);
} catch (error) {
const message = `Input error for json field '${field}': ${
error instanceof Error ? error.message : ""
}`;
throw Error(message);
}
}
}
// delete unneccesary fields
if (!options[field]) {
delete options[field];
}
});
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
await sendTransaction(
account,
{
TransactionType,
...options,
},
{ logPrefix }
);
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({ type: "error", message: error.message });
}
}
setTxIsLoading(false);
}, [
header,
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
snap.accounts,
txFields,
txIsDisabled,
]);
const resetState = useCallback(() => {
setSelectedAccount(null);
setSelectedDestAccount(null);
setSelectedTransaction(null);
setTxFields({});
setTxIsDisabled(false);
setTxIsLoading(false);
}, []);
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(
(k) => !usualFields.includes(k)
) as OtherFields;
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={(tt) => setSelectedTransaction(tt as any)}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={(acc) => setSelectedAccount(acc as any)}
/>
</Flex>
{txFields.Amount !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={(e) =>
setTxFields({
...txFields,
Amount: { type: "currency", value: e.target.value },
})
}
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc) => setSelectedDestAccount(acc as any)}
/>
</Flex>
)}
{otherFields.map((field) => {
let _value = txFields[field];
let value = typeof _value === "object" ? _value.value : _value;
value =
typeof value === "object"
? JSON.stringify(value)
: value?.toLocaleString();
let isCurrency =
typeof _value === "object" && _value.type === "currency";
return (
<Flex
key={field}
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isCurrency ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={(e) =>
setTxFields({
...txFields,
[field]:
typeof _value === "object"
? { ..._value, value: e.target.value }
: e.target.value,
})
}
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
);
})}
</Flex>
</Container>
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
}}
>
<Button outline>VIEW AS JSON</Button>
<Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
);
};
const Test = () => {
const { transactionLogs } = useSnapshot(state);
const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
const { transactions, activeHeader } = useSnapshot(transactionsState);
return (
<Container css={{ px: 0 }}>
<Split
@@ -376,7 +30,7 @@ const Test = () => {
gutterSize={4}
gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }}
onDragEnd={(e) => saveSplit("testVertical", e)}
onDragEnd={e => saveSplit("testVertical", e)}
>
<Flex
row
@@ -398,23 +52,29 @@ const Test = () => {
width: "100%",
height: "100%",
}}
onDragEnd={(e) => saveSplit("testHorizontal", e)}
onDragEnd={e => saveSplit("testHorizontal", e)}
>
<Box css={{ width: "55%", px: "$2" }}>
<Tabs
activeHeader={activeHeader}
// TODO make header a required field
onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header;
}}
keepAllAlive
forceDefaultExtension
defaultExtension=".json"
onCreateNewTab={(name) =>
setTabHeaders(tabHeaders.concat(name))
}
onCloseTab={(index) =>
setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))
onCreateNewTab={header => modifyTransaction(header, {})}
onCloseTab={(idx, header) =>
header && modifyTransaction(header, undefined)
}
>
{tabHeaders.map((header) => (
{transactions.map(({ header, state }) => (
<Tab key={header} header={header}>
<Transaction header={header} />
<Transaction
state={state}
header={header}
/>
</Tab>
))}
</Tabs>

View File

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

View File

@@ -4,19 +4,21 @@ import toast from "react-hot-toast";
import state, { IAccount } from "../index";
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
import { SetHookData } from "../../components/SetHookDialog";
import { Link } from "../../components";
import { ref } from "valtio";
export const sha256 = async (string: string) => {
const utf8 = new TextEncoder().encode(string);
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8);
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
.map(bytes => bytes.toString(16).padStart(2, "0"))
.join("");
return hashHex;
}
};
function toHex(str: string) {
var result = '';
var result = "";
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16);
}
@@ -51,7 +53,10 @@ function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
* hex string, signs the transaction and deploys it to
* Hooks testnet.
*/
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
export const deployHook = async (
account: IAccount & { name?: string },
data: SetHookData
) => {
if (
!state.files ||
state.files.length === 0 ||
@@ -69,7 +74,15 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value);
const { HookParameters } = data;
const filteredHookParameters = HookParameters.filter(hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue)?.map(aa => ({ HookParameter: { HookParameterName: toHex(aa.HookParameter.HookParameterName || ''), HookParameterValue: toHex(aa.HookParameter.HookParameterValue || '') } }));
const filteredHookParameters = HookParameters.filter(
hp =>
hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
)?.map(aa => ({
HookParameter: {
HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
HookParameterValue: aa.HookParameter.HookParameterValue || "",
},
}));
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
// return {
// HookGrant: {
@@ -97,16 +110,18 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
HookApiVersion: 0,
Flags: 1,
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
...(filteredHookParameters.length > 0 && { HookParameters: filteredHookParameters }),
}
}
]
...(filteredHookParameters.length > 0 && {
HookParameters: filteredHookParameters,
}),
},
},
],
};
const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find(
(acc) => acc.address === account.address
acc => acc.address === account.address
);
if (currentAccount) {
currentAccount.isLoading = true;
@@ -125,12 +140,28 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
});
state.deployLogs.push({
type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
message: ref(
<>
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
Validated ledger index:{" "}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.validated_ledger_index}`}
target="_blank"
rel="noopener noreferrer"
>
{submitRes.validated_ledger_index}
</Link>
</>
),
// message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
});
} else {
state.deployLogs.push({
type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`,
message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception
}`,
});
}
} catch (err) {
@@ -152,10 +183,10 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
return;
}
const currentAccount = state.accounts.find(
(acc) => acc.address === account.address
acc => acc.address === account.address
);
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
return
return;
}
if (typeof window !== "undefined") {
const tx = {
@@ -168,9 +199,9 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
Hook: {
CreateCode: "",
Flags: 1,
}
}
]
},
},
],
};
const keypair = derive.familySeed(account.secret);
@@ -188,7 +219,7 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
});
if (submitRes.engine_result === "tesSUCCESS") {
toast.success('Hook deleted successfully ✅', { id: toastId })
toast.success("Hook deleted successfully ✅", { id: toastId });
state.deployLogs.push({
type: "success",
message: "Hook deleted successfully ✅",
@@ -199,15 +230,20 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
});
currentAccount.hooks = [];
} else {
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, { id: toastId })
toast.error(
`${submitRes.engine_result_message || submitRes.error_exception}`,
{ id: toastId }
);
state.deployLogs.push({
type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`,
message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception
}`,
});
}
} catch (err) {
console.log(err);
toast.error('Error occured while deleting hoook', { id: toastId })
toast.error("Error occured while deleting hoook", { id: toastId });
state.deployLogs.push({
type: "error",
message: "Error occured while deleting hook",
@@ -218,4 +254,4 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
}
return submitRes;
}
};
};

View File

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

View File

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

View File

@@ -39,9 +39,10 @@ export interface IAccount {
export interface ILog {
type: "error" | "warning" | "log" | "success";
message: string;
message: string | JSX.Element;
key?: string;
jsonData?: any,
timestamp?: string;
timestring?: string;
link?: string;
linkText?: string;
defaultCollapsed?: boolean
@@ -73,6 +74,10 @@ export interface IState {
mainModalOpen: boolean;
mainModalShowed: boolean;
accounts: IAccount[];
compileOptions: {
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
strip: boolean
}
}
// let localStorageState: null | string = null;
@@ -102,6 +107,10 @@ let initialState: IState = {
mainModalOpen: false,
mainModalShowed: false,
accounts: [],
compileOptions: {
optimizationLevel: '-O0',
strip: false
}
};
let localStorageAccounts: string | null = null;
@@ -159,3 +168,5 @@ if (typeof window !== "undefined") {
});
}
export default state
export * from './transactions'

59
state/transactions.ts Normal file
View File

@@ -0,0 +1,59 @@
import { proxy } from 'valtio';
import { TransactionState } from '../components/Transaction';
import { deepEqual } from '../utils/object';
export const defaultTransaction: TransactionState = {
selectedTransaction: null,
selectedAccount: null,
selectedDestAccount: null,
txIsLoading: false,
txIsDisabled: false,
txFields: {},
};
export const transactionsState = proxy({
transactions: [
{
header: "test1.json",
state: defaultTransaction,
},
],
activeHeader: "test1.json"
});
/**
* Simple transaction state changer
* @param header Unique key and tab name for the transaction tab
* @param partialTx partial transaction state, `{}` resets the state and `undefined` deletes the transaction
*/
export const modifyTransaction = (
header: string,
partialTx?: Partial<TransactionState>
) => {
const tx = transactionsState.transactions.find(tx => tx.header === header);
if (partialTx === undefined) {
transactionsState.transactions = transactionsState.transactions.filter(
tx => tx.header !== header
);
return;
}
if (!tx) {
transactionsState.transactions.push({
header,
state: {
...defaultTransaction,
...partialTx,
},
});
return;
}
Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definetly safe!
const s = tx.state as any;
const p = partialTx as any;
if (!deepEqual(s[k], p[k])) s[k] = p[k];
});
};

View File

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

24
utils/object.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

1213
yarn.lock

File diff suppressed because it is too large Load Diff