Compare commits

...

81 Commits

Author SHA1 Message Date
Valtteri Karesto
e0ed31f220 Added experimental to label 2022-05-11 08:50:00 +03:00
muzamil
eba183497f Merge pull request #176 from eqlabs/feat/json-transactions
Json transactions
2022-05-10 18:15:00 +05:30
Valtteri Karesto
4378afa9a1 Merge pull request #185 from eqlabs/feat/add-cleaner-ui
Feat/add cleaner UI
2022-05-10 14:43:04 +03:00
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
muzam1l
53f2a71b08 minor changes 2022-05-05 21:02:33 +05:30
muzam1l
866f6257f1 json schema 2022-05-05 20:00:10 +05:30
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
muzam1l
386619619b support object Amount 2022-05-04 16:57:32 +05:30
muzam1l
d8bf10d0b8 fix handling Destination field 2022-05-04 16:12:50 +05:30
muzam1l
d18c893025 Replace native alerts 2022-05-04 14:38:59 +05:30
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
muzam1l
5e997044ed alert dialog component 2022-05-02 21:05:18 +05:30
muzam1l
e88720327e Allow using only imported accounts 2022-04-28 18:51:50 +05:30
muzam1l
bf568c3f46 Update button text 2022-04-28 18:33:20 +05:30
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
muzam1l
1d3bd128f8 Implement auto save 2022-04-27 23:18:00 +05:30
muzam1l
ab1f45febd Editable json and unified state. 2022-04-27 18:34:28 +05:30
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
muzam1l
56a9806b70 add path to tx editor and refactor its value providing 2022-04-25 15:47:58 +05:30
muzam1l
b3f2d0fb6d Readonly tx json view 2022-04-22 18:46:35 +05:30
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
Valtteri Karesto
f49d69e75d Merge pull request #172 from eqlabs/fix/regression-#171
Fix/regression #171
2022-04-14 15:41:59 +03:00
Valtteri Karesto
da4b2e68ab Fix issue #171 2022-04-14 15:28:28 +03:00
Valtteri Karesto
5557b1bcba Remove console.logs 2022-04-14 15:27:08 +03:00
Valtteri Karesto
f4b5f98a44 Merge pull request #169 from eqlabs/feat/add-namespace-to-sethook
Add namespace to sethook modal
2022-04-14 15:19:08 +03:00
Valtteri Karesto
b1d39740de Changed labels, added default value to form so the initial render has correct value 2022-04-14 15:10:38 +03:00
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
Valtteri Karesto
16cbdafb27 Add computed sha256 field to sethook form 2022-04-14 11:52:24 +03:00
Valtteri Karesto
5559fb7be3 rename hash to sha256 2022-04-14 11:51:56 +03:00
Valtteri Karesto
3c4305127b Use user input namespace value 2022-04-13 23:58:02 +03:00
Valtteri Karesto
2a76fa0c35 Add namespace field to set hook modal 2022-04-13 23:57:47 +03:00
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
Valtteri Karesto
919c4e173c Merge pull request #166 from eqlabs/feat/remove-peek-from-context-menu
Feat/remove peek from context menu
2022-04-12 09:19:19 +03:00
Valtteri Karesto
650324f434 Hacky way to hide Peek from context menu 2022-04-11 17:07:25 +03:00
Valtteri Karesto
74db96e8a5 add ; 2022-04-11 17:07:13 +03:00
Valtteri Karesto
c99c821081 Update monaco editor and next-themes 2022-04-11 17:07:06 +03:00
Valtteri Karesto
e53a533026 Merge pull request #161 from eqlabs/fix/add-account-creation-error
Fixes issue #68
2022-04-11 14:54:12 +03:00
Valtteri Karesto
5f118e00cb Added error check 2022-04-11 11:46:49 +03:00
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
Valtteri Karesto
e795ce4472 Fixes issue #68 2022-04-06 14:15:53 +03:00
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
44 changed files with 14602 additions and 1210 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

@@ -0,0 +1,75 @@
import { FC, ReactNode } from "react";
import { proxy, useSnapshot } from "valtio";
import Button from "../Button";
import Flex from "../Flex";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from "./primitive";
export interface AlertState {
isOpen: boolean;
title?: string;
body?: ReactNode;
cancelText?: string;
confirmText?: string;
confirmPrefix?: ReactNode;
onConfirm?: () => any;
onCancel?: () => any;
}
export const alertState = proxy<AlertState>({
isOpen: false,
});
const Alert: FC = () => {
const {
title = "Are you sure?",
isOpen,
body,
cancelText,
confirmText = "Ok",
confirmPrefix,
onCancel,
onConfirm,
} = useSnapshot(alertState);
return (
<AlertDialog
open={isOpen}
onOpenChange={value => (alertState.isOpen = value)}
>
<AlertDialogContent>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{body}</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
{(cancelText || onCancel) && (
<AlertDialogCancel asChild>
<Button css={{ minWidth: "$16" }} outline onClick={onCancel}>
{cancelText || "Cancel"}
</Button>
</AlertDialogCancel>
)}
<AlertDialogAction asChild>
<Button
css={{ minWidth: "$16" }}
variant="primary"
onClick={async () => {
await onConfirm?.();
alertState.isOpen = false;
}}
>
{confirmPrefix}
{confirmText}
</Button>
</AlertDialogAction>
</Flex>
</AlertDialogContent>
</AlertDialog>
);
};
export default Alert;

View File

@@ -1,7 +1,7 @@
import React from "react";
import { blackA } from "@radix-ui/colors";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { styled, keyframes } from "../stitches.config";
import { styled, keyframes } from "../../stitches.config";
const overlayShow = keyframes({
"0%": { opacity: 0 },
@@ -75,7 +75,7 @@ const StyledDescription = styled(AlertDialogPrimitive.Description, {
marginBottom: 20,
color: "$mauve11",
lineHeight: 1.5,
fontSize: "$sm",
fontSize: "$md",
});
// Exports

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,18 +47,11 @@ 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 {
AlertDialog,
AlertDialogContent,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogCancel,
AlertDialogAction,
} from "./AlertDialog";
import { styled } from "../stitches.config";
import { showAlert } from "../state/actions/showAlert";
const ErrorText = styled(Text, {
color: "$error",
@@ -68,7 +61,6 @@ const ErrorText = styled(Text, {
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
const snap = useSnapshot(state);
const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null);
@@ -87,13 +79,29 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
setNewfileError(null);
}, [filename, setNewfileError]);
const showNewGistAlert = () => {
showAlert("Are you sure?", {
body: (
<>
This action will create new <strong>public</strong> Github Gist from
your current saved files. You can delete gist anytime from your GitHub
Gists page.
</>
),
cancelText: "Cancel",
confirmText: "Create new Gist",
confirmPrefix: <FilePlus size="15px" />,
onConfirm: () => syncToGist(session, true),
});
};
const validateFilename = useCallback(
(filename: string): { error: string | null } => {
// check if filename already exists
if (!filename) {
return { error: "You need to add filename" };
}
if (snap.files.find((file) => file.name === filename)) {
if (snap.files.find(file => file.name === filename)) {
return { error: "Filename already exists." };
}
@@ -222,11 +230,11 @@ 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)}
onKeyPress={(e) => {
onChange={e => setFilename(e.target.value)}
onKeyPress={e => {
if (e.key === "Enter") {
handleConfirm();
}
@@ -416,7 +424,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
if (snap.gistOwner === session?.user.username) {
syncToGist(session);
} else {
setCreateNewAlertOpen(true);
showNewGistAlert();
}
}}
>
@@ -466,7 +474,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<DropdownMenuItem
disabled={status !== "authenticated"}
onClick={() => {
setCreateNewAlertOpen(true);
showNewGistAlert();
}}
>
<FilePlus size="16px" /> Create as a new Gist
@@ -486,34 +494,6 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
) : null}
</Container>
</Flex>
<AlertDialog
open={createNewAlertOpen}
onOpenChange={(value) => setCreateNewAlertOpen(value)}
>
<AlertDialogContent>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action will create new <strong>public</strong> Github Gist from
your current saved files. You can delete gist anytime from your
GitHub Gists page.
</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
<AlertDialogCancel asChild>
<Button outline>Cancel</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
variant="primary"
onClick={() => {
syncToGist(session, true);
}}
>
<FilePlus size="15px" /> Create new Gist
</Button>
</AlertDialogAction>
</Flex>
</AlertDialogContent>
</AlertDialog>
<Dialog open={editorSettingsOpen} onOpenChange={setEditorSettingsOpen}>
<DialogTrigger asChild>
@@ -524,13 +504,13 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<DialogContent>
<DialogTitle>Editor settings</DialogTitle>
<DialogDescription>
<label>Tab size</label>
<Label>Tab size</Label>
<Input
type="number"
min="1"
value={editorSettings.tabSize}
onChange={(e) =>
setEditorSettings((curr) => ({
onChange={e =>
setEditorSettings(curr => ({
...curr,
tabSize: Number(e.target.value),
}))

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(
@@ -226,6 +222,22 @@ const HooksEditor = () => {
}
});
// Hacky way to hide Peek menu
editor.onContextMenu((e) => {
const host =
document.querySelector<HTMLElement>(".shadow-root-host");
const contextMenuItems =
host?.shadowRoot?.querySelectorAll("li.action-item");
contextMenuItems?.forEach((k) => {
// If menu item contains "Peek" lets hide it
if (k.querySelector(".action-label")?.textContent === "Peek") {
// @ts-expect-error
k["style"].display = "none";
}
});
});
validateWritability(editor);
}}
theme={theme === "dark" ? "dark" : "light"}

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
@@ -59,6 +60,8 @@ export const Input = styled("input", {
},
"&:read-only": {
backgroundColor: "$mauve2",
color: "$text",
opacity: 0.8,
"&:focus": {
boxShadow: "inset 0px 0px 0px 1px $colors$mauve7",
},
@@ -156,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

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { Plus, Trash, X } from "phosphor-react";
import Button from "./Button";
import Box from "./Box";
@@ -11,7 +11,7 @@ import {
DialogClose,
DialogTrigger,
} from "./Dialog";
import { Input } from "./Input";
import { Input, Label } from "./Input";
import {
Controller,
SubmitHandler,
@@ -25,6 +25,7 @@ import type { IAccount } from "../state";
import { useSnapshot } from "valtio";
import state from "../state";
import toast from "react-hot-toast";
import { sha256 } from "../state/actions/deployHook";
const transactionOptions = Object.keys(tts).map((key) => ({
label: key,
@@ -36,6 +37,7 @@ export type SetHookData = {
value: keyof TTS;
label: string;
}[];
HookNamespace: string;
HookParameters: {
HookParameter: {
HookParameterName: string;
@@ -57,12 +59,26 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
register,
handleSubmit,
control,
// formState: { errors },
} = useForm<SetHookData>();
watch,
setValue,
formState: { errors },
} = useForm<SetHookData>({
defaultValues: {
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,
@@ -71,6 +87,19 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
// control,
// name: "HookGrants", // unique name for your Field Array
// });
const [hashedNamespace, setHashedNamespace] = useState("");
const namespace = watch(
"HookNamespace",
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
);
const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace);
setHashedNamespace(hashedVal.toUpperCase());
}, [namespace]);
useEffect(() => {
calculateHashedValue();
}, [namespace, calculateHashedValue]);
if (!account) {
return null;
}
@@ -89,6 +118,7 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
}
toast.error(`Transaction failed! (${res?.engine_result_message})`);
};
return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<DialogTrigger asChild>
@@ -111,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}
@@ -130,9 +160,28 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
/>
</Box>
<Box css={{ width: "100%" }}>
<label style={{ marginBottom: "10px", display: "block" }}>
<Label>Hook Namespace Seed</Label>
<Input
{...register("HookNamespace", { required: true })}
autoComplete={"off"}
defaultValue={
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
}
/>
{errors.HookNamespace?.type === "required" && (
<Box css={{ display: "inline", color: "$red11" }}>
Namespace is required
</Box>
)}
<Box css={{ mt: "$3" }}>
<Label>Hook Namespace (sha256)</Label>
<Input readOnly value={hashedNamespace} />
</Box>
</Box>
<Box css={{ width: "100%" }}>
<Label style={{ marginBottom: "10px", display: "block" }}>
Hook parameters
</label>
</Label>
<Stack>
{fields.map((field, index) => (
<Stack key={field.id}>
@@ -144,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>

View File

@@ -0,0 +1,184 @@
import { Play } from "phosphor-react";
import { FC, useCallback, useEffect, useMemo } from "react";
import { useSnapshot } from "valtio";
import state from "../../state";
import {
modifyTransaction,
prepareState,
prepareTransaction,
TransactionState,
} from "../../state/transactions";
import { sendTransaction } from "../../state/actions";
import Box from "../Box";
import Button from "../Button";
import Flex from "../Flex";
import { TxJson } from "./json";
import { TxUI } from "./ui";
export interface TransactionProps {
header: string;
state: TransactionState;
}
const Transaction: FC<TransactionProps> = ({
header,
state: txState,
...props
}) => {
const { accounts, editorSettings } = useSnapshot(state);
const {
selectedAccount,
selectedTransaction,
txIsDisabled,
txIsLoading,
viewType,
editorSavedValue,
editorValue,
} = txState;
const setState = useCallback(
(pTx?: Partial<TransactionState>) => {
return modifyTransaction(header, pTx);
},
[header]
);
const prepareOptions = useCallback(
(state: TransactionState = txState) => {
const {
selectedTransaction,
selectedDestAccount,
selectedAccount,
txFields,
} = state;
const TransactionType = selectedTransaction?.value || null;
const Destination =
selectedDestAccount?.value ||
("Destination" in txFields ? null : undefined);
const Account = selectedAccount?.value || null;
return prepareTransaction({
...txFields,
TransactionType,
Destination,
Account,
});
},
[txState]
);
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = selectedAccount?.value;
if (!account || !transactionType || txIsLoading) {
setState({ txIsDisabled: true });
} else {
setState({ txIsDisabled: false });
}
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading]);
const submitTest = useCallback(async () => {
let st: TransactionState | undefined;
if (viewType === "json") {
// save the editor state first
const pst = prepareState(editorValue || '', txState);
if (!pst) return;
st = setState(pst);
}
const account = accounts.find(
acc => acc.address === selectedAccount?.value
);
if (txIsDisabled) return;
setState({ txIsLoading: true });
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
try {
if (!account) {
throw Error("Account must be selected from imported accounts!");
}
const options = prepareOptions(st);
if (options.Destination === null) {
throw Error("Destination account cannot be null")
}
await sendTransaction(account, options, { logPrefix });
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({
type: "error",
message: `${logPrefix}${error.message}`,
});
}
}
setState({ txIsLoading: false });
}, [viewType, accounts, txIsDisabled, setState, header, editorValue, txState, selectedAccount?.value, prepareOptions]);
const resetState = useCallback(() => {
modifyTransaction(header, { viewType }, { replaceState: true });
}, [header, viewType]);
const jsonValue = useMemo(
() =>
editorSavedValue ||
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize),
[editorSavedValue, editorSettings.tabSize, prepareOptions]
);
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
{viewType === "json" ? (
<TxJson
value={jsonValue}
header={header}
state={txState}
setState={setState}
/>
) : (
<TxUI state={txState} setState={setState} />
)}
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
mb: "$1",
}}
>
<Button
onClick={() => {
if (viewType === "ui") {
setState({ editorSavedValue: null, viewType: "json" });
} else setState({ viewType: "ui" });
}}
outline
>
{viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"}
</Button>
<Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
);
};
export default Transaction;

View File

@@ -0,0 +1,205 @@
import Editor, { loader, useMonaco } from "@monaco-editor/react";
import { FC, useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import dark from "../../theme/editor/amy.json";
import light from "../../theme/editor/xcode_default.json";
import { useSnapshot } from "valtio";
import state, {
prepareState,
transactionsData,
TransactionState,
} from "../../state";
import Text from "../Text";
import Flex from "../Flex";
import { Link } from "..";
import { showAlert } from "../../state/actions/showAlert";
import { parseJSON } from "../../utils/json";
import { extractSchemaProps } from "../../utils/schema";
import amountSchema from "../../content/amount-schema.json";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
interface JsonProps {
value?: string;
header?: string;
setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState;
}
export const TxJson: FC<JsonProps> = ({
value = "",
state: txState,
header,
setState,
}) => {
const { editorSettings, accounts } = useSnapshot(state);
const { editorValue = value, selectedTransaction } = txState;
const { theme } = useTheme();
const [hasUnsaved, setHasUnsaved] = useState(false);
useEffect(() => {
setState({ editorValue: value });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
useEffect(() => {
if (editorValue === value) setHasUnsaved(false);
else setHasUnsaved(true);
}, [editorValue, value]);
const saveState = (value: string, txState: TransactionState) => {
const tx = prepareState(value, txState);
if (tx) setState(tx);
};
const discardChanges = () => {
showAlert("Confirm", {
body: "Are you sure to discard these changes?",
confirmText: "Yes",
onConfirm: () => setState({ editorValue: value }),
});
};
const onExit = (value: string) => {
const options = parseJSON(value);
if (options) {
saveState(value, txState);
return;
}
showAlert("Error!", {
body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
confirmText: "Discard",
onConfirm: () => setState({ editorValue: value }),
onCancel: () => setState({ viewType: "json", editorSavedValue: value }),
});
};
const path = `file:///${header}`;
const monaco = useMonaco();
const getSchemas = useCallback((): any[] => {
const tt = selectedTransaction?.value;
const txObj = transactionsData.find(td => td.TransactionType === tt);
let genericSchemaProps: any;
if (txObj) {
genericSchemaProps = extractSchemaProps(txObj);
} else {
genericSchemaProps = transactionsData.reduce(
(cumm, td) => ({
...cumm,
...extractSchemaProps(td),
}),
{}
);
}
return [
{
uri: "file:///main-schema.json", // id of the first schema
fileMatch: ["**.json"], // associate with our model
schema: {
title: header,
type: "object",
required: ["TransactionType", "Account"],
properties: {
...genericSchemaProps,
TransactionType: {
title: "Transaction Type",
enum: transactionsData.map(td => td.TransactionType),
},
Account: {
$ref: "file:///account-schema.json",
},
Destination: {
anyOf: [
{
$ref: "file:///account-schema.json",
},
{
type: "string",
title: "Destination Account",
},
],
},
Amount: {
$ref: "file:///amount-schema.json",
},
},
},
},
{
uri: "file:///account-schema.json",
schema: {
type: "string",
title: "Account type",
enum: accounts.map(acc => acc.address),
},
},
{
...amountSchema,
},
];
}, [accounts, header, selectedTransaction?.value]);
useEffect(() => {
if (!monaco) return;
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: getSchemas(),
});
}, [getSchemas, monaco]);
return (
<Flex
fluid
column
css={{ height: "calc(100% - 45px)", position: "relative" }}
>
<Editor
className="hooks-editor"
language={"json"}
path={path}
height="100%"
beforeMount={monaco => {
monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme("light", light as any);
}}
value={editorValue}
onChange={val => setState({ editorValue: val })}
onMount={(editor, monaco) => {
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
});
// register onExit cb
const model = editor.getModel();
model?.onWillDispose(() => onExit(model.getValue()));
// set json defaults
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: getSchemas(),
});
}}
theme={theme === "dark" ? "dark" : "light"}
/>
{hasUnsaved && (
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
This file has unsaved changes.{" "}
<Link onClick={() => saveState(editorValue, txState)}>save</Link>{" "}
<Link onClick={discardChanges}>discard</Link>
</Text>
)}
</Flex>
);
};

View File

@@ -0,0 +1,213 @@
import { FC } from "react";
import Container from "../Container";
import Flex from "../Flex";
import Input from "../Input";
import Select from "../Select";
import Text from "../Text";
import {
SelectOption,
TransactionState,
transactionsData,
TxFields,
} from "../../state/transactions";
import { useSnapshot } from "valtio";
import state from "../../state";
import { streamState } from "../DebugStream";
interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState;
}
export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
const { accounts } = useSnapshot(state);
const {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
} = txState;
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const destAccountOptions: SelectOption[] = accounts
.map(acc => ({
label: acc.name,
value: acc.address,
}))
.filter(acc => acc.value !== selectedAccount?.value);
const resetOptions = (tt: string) => {
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt
);
if (!txFields) return setState({ txFields: {} });
const _txFields = Object.keys(txFields)
.filter(key => !["TransactionType", "Account", "Sequence"].includes(key))
.reduce<TxFields>(
(tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf),
{}
);
if (!_txFields.Destination) setState({ selectedDestAccount: null });
setState({ txFields: _txFields });
};
const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc });
streamState.selectedAccount = acc;
};
const handleChangeTxType = (tt: SelectOption) => {
setState({ selectedTransaction: tt });
resetOptions(tt.value);
};
const specialFields = ["TransactionType", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(
k => !specialFields.includes(k)
) as [keyof TxFields];
return (
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={(tt: any) => handleChangeTxType(tt)}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/>
</Flex>
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/>
</Flex>
)}
{otherFields.map(field => {
let _value = txFields[field];
let value: string | undefined;
if (typeof _value === "object") {
if (_value.$type === "json" && typeof _value.$value === "object") {
value = JSON.stringify(_value.$value);
} else {
value = _value.$value.toString();
}
} else {
value = _value?.toString();
}
let isXrp = typeof _value === "object" && _value.$type === "xrp";
return (
<Flex
key={field}
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isXrp ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e => {
setState({
txFields: {
...txFields,
[field]:
typeof _value === "object"
? { ..._value, $value: e.target.value }
: e.target.value,
},
});
}}
css={{ width: "70%", flex: "inherit" }}
/>
</Flex>
);
})}
</Flex>
</Container>
);
};

View File

@@ -4,10 +4,10 @@ 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";
export * from "./AlertDialog/primitive";
export { default as Box } from "./Box";
export { default as Button } from "./Button";
export { default as Pre } from "./Pre";

View File

@@ -0,0 +1,50 @@
{
"uri": "file:///amount-schema.json",
"title": "Amount",
"description": "Specify xrp in drops and tokens as objects.",
"schema": {
"anyOf": [
{
"type": [
"number",
"string"
],
"exclusiveMinimum": 0,
"maximum": "100000000000000000"
},
{
"type": "object",
"properties": {
"currency": {
"description": "Arbitrary currency code for the token. Cannot be XRP."
},
"value": {
"type": [
"string",
"number"
],
"description": "Quoted decimal representation of the amount of the token."
},
"issuer": {
"type": "string",
"description": "Generally, the account that issues this token. In special cases, this can refer to the account that holds the token instead."
}
}
}
],
"defaultSnippets": [
{
"label": "Xrp",
"body": "1000000"
},
{
"label": "Token",
"body": {
"currency": "${1:13.1}",
"value": "${2:FOO}",
"description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
}
}
]
}
}

View File

@@ -27,8 +27,8 @@
"Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"TransactionType": "CheckCash",
"Amount": {
"value": "100",
"type": "currency"
"$value": "100",
"$type": "xrp"
},
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12"
@@ -61,8 +61,8 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCreate",
"Amount": {
"value": "100",
"type": "currency"
"$value": "100",
"$type": "xrp"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958,
@@ -99,8 +99,8 @@
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": {
"value": "100",
"type": "currency"
"$value": "100",
"$type": "xrp"
},
"Flags": 1
},
@@ -122,8 +122,8 @@
"Sequence": 8,
"TakerGets": "6000000",
"Amount": {
"value": "100",
"type": "currency"
"$value": "100",
"$type": "xrp"
}
},
{
@@ -131,8 +131,8 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Amount": {
"value": "100",
"type": "currency"
"$value": "100",
"$type": "xrp"
},
"Fee": "12",
"Flags": 2147483648,
@@ -142,8 +142,8 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelCreate",
"Amount": {
"value": "100",
"type": "currency"
"$value": "100",
"$type": "xrp"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400,
@@ -157,8 +157,8 @@
"TransactionType": "PaymentChannelFund",
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": {
"value": "200",
"type": "currency"
"$value": "200",
"$type": "xrp"
},
"Expiration": 543171558
},
@@ -176,8 +176,8 @@
"Fee": "12",
"SignerQuorum": 3,
"SignerEntries": {
"type": "json",
"value": [
"$type": "json",
"$value": [
{
"SignerEntry": {
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
@@ -213,8 +213,8 @@
"Flags": 262144,
"LastLedgerSequence": 8007750,
"Amount": {
"value": "100",
"type": "currency"
"$value": "100",
"$type": "xrp"
},
"Sequence": 12
}

12057
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,13 +12,16 @@
"dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.3.1",
"@monaco-editor/react": "^4.4.1",
"@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1",
"@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",
@@ -29,10 +32,10 @@
"jszip": "^3.7.1",
"lodash.uniqby": "^4.7.0",
"lodash.xor": "^4.5.0",
"monaco-editor": "^0.30.1",
"monaco-editor": "^0.33.0",
"next": "^12.0.4",
"next-auth": "^4.0.0-beta.5",
"next-themes": "^0.0.15",
"next-themes": "^0.1.1",
"normalize-url": "^7.0.2",
"octokit": "^1.7.0",
"pako": "^2.0.4",

View File

@@ -16,9 +16,10 @@ import state from "../state";
import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en.json";
import { useSnapshot } from "valtio";
import Alert from '../components/AlertDialog';
TimeAgo.setDefaultLocale(en.locale);
TimeAgo.addLocale(en)
TimeAgo.addLocale(en);
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
const router = useRouter();
@@ -140,6 +141,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
})(),
}}
/>
<Alert />
</ThemeProvider>
</SessionProvider>
</IdProvider>

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 (experimental){" "}
<Popover
css={{
maxWidth: "240px",
lineHeight: "1.3",
a: {
color: "$purple11",
},
".dark &": {
backgroundColor: "$black !important",
".arrow": {
fill: "$colors$black",
},
},
}}
content="Cleaner removes unwanted compiler-provided exports and functions from a wasm binary to make it (more) suitable for being used as a Hook"
>
<Flex
css={{
position: "relative",
top: "-1px",
mx: "$1",
backgroundColor: "$mauve8",
borderRadius: "$full",
cursor: "pointer",
width: "16px",
height: "16px",
alignItems: "center",
justifyContent: "center",
}}
>
?
</Flex>
</Popover>
</Label>
<Switch
css={{ mt: "$2" }}
checked={snap.compileOptions.strip}
onCheckedChange={(checked) => {
state.compileOptions.strip = checked;
}}
>
<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";
const hash = async (string: string) => {
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 ||
@@ -66,12 +71,18 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
if (!state.client) {
return;
}
const HookNamespace = await hash(arrayBufferToHex(
state.files?.[state.active]?.compiledContent
).toUpperCase());
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: {
@@ -99,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;
@@ -127,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) {
@@ -154,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 = {
@@ -170,9 +199,9 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
Hook: {
CreateCode: "",
Flags: 1,
}
}
]
},
},
],
};
const keypair = derive.familySeed(account.secret);
@@ -190,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 ✅",
@@ -201,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",
@@ -220,4 +254,4 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
}
return submitRes;
}
};
};

View File

@@ -28,18 +28,19 @@ export const fetchFiles = (gistId: string) => {
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
if (resHeader.ok) {
resHeaderJson = await resHeader.json();
const files = {
...res.data.files,
'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: resHeaderJson.hookapi, language: 'C' },
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: resHeaderJson.hookmacro, language: 'C' },
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: resHeaderJson.sfcodes, language: 'C' },
};
res.data.files = files;
}
} catch (err) {
console.log(err)
}
const files = {
...res.data.files,
'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: resHeaderJson.hookapi, language: 'C' },
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: resHeaderJson.hookmacro, language: 'C' },
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: resHeaderJson.sfcodes, language: 'C' },
};
res.data.files = files;
return res;
// If you want to load templates from GIST instad, uncomment the code below and comment the code above.

View File

@@ -1,5 +1,5 @@
import toast from "react-hot-toast";
import { derive } from "xrpl-accountlib";
import { derive, XRPL_Account } from "xrpl-accountlib";
import state from '../index';
import { names } from './addFaucetAccount';
@@ -12,8 +12,18 @@ export const importAccount = (secret: string) => {
if (state.accounts.find((acc) => acc.secret === secret)) {
return toast.error("Account already added!");
}
const account = derive.familySeed(secret);
if (!account.secret.familySeed) {
let account: XRPL_Account | null = null;
try {
account = derive.familySeed(secret);
} catch (err: any) {
if (err?.message) {
toast.error(err.message)
} else {
toast.error('Error occured while importing account')
}
return;
}
if (!account || !account.secret.familySeed) {
return toast.error(`Couldn't create account!`);
}
state.accounts.push({

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

@@ -0,0 +1,23 @@
import { ref } from 'valtio';
import { AlertState, alertState } from "../../components/AlertDialog";
export const showAlert = (title: string, opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}) => {
const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts
const body = (_body && typeof _body === 'object') ? ref(_body) : _body
const confirmPrefix = (_confirmPrefix && typeof _confirmPrefix === 'object') ? ref(_confirmPrefix) : _confirmPrefix
const nwState: AlertState = {
isOpen: true,
title,
body,
confirmPrefix,
cancelText: undefined,
confirmText: undefined,
onCancel: undefined,
onConfirm: undefined,
...rest,
}
Object.entries(nwState).forEach(([key, value]) => {
(alertState as any)[key] = value
})
}

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'

225
state/transactions.ts Normal file
View File

@@ -0,0 +1,225 @@
import { proxy } from 'valtio';
import { deepEqual } from '../utils/object';
import transactionsData from "../content/transactions.json";
import state from '.';
import { showAlert } from "../state/actions/showAlert";
import { parseJSON } from '../utils/json';
export type SelectOption = {
value: string;
label: string;
};
export interface TransactionState {
selectedTransaction: SelectOption | null;
selectedAccount: SelectOption | null;
selectedDestAccount: SelectOption | null;
txIsLoading: boolean;
txIsDisabled: boolean;
txFields: TxFields;
viewType: 'json' | 'ui',
editorSavedValue: null | string,
editorValue?: string
}
export type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
export const defaultTransaction: TransactionState = {
selectedTransaction: null,
selectedAccount: null,
selectedDestAccount: null,
txIsLoading: false,
txIsDisabled: false,
txFields: {},
viewType: 'ui',
editorSavedValue: null
};
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, `undefined` deletes the transaction
*
*/
export const modifyTransaction = (
header: string,
partialTx?: Partial<TransactionState>,
opts: { replaceState?: boolean } = {}
) => {
const tx = transactionsState.transactions.find(tx => tx.header === header);
if (partialTx === undefined) {
transactionsState.transactions = transactionsState.transactions.filter(
tx => tx.header !== header
);
return;
}
if (!tx) {
const state = {
...defaultTransaction,
...partialTx,
}
transactionsState.transactions.push({
header,
state,
});
return state;
}
if (opts.replaceState) {
const repTx: TransactionState = {
...defaultTransaction,
...partialTx,
}
tx.state = repTx
return repTx
}
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];
});
return tx.state
};
// state to tx options
export const prepareTransaction = (data: any) => {
let options = { ...data };
(Object.keys(options)).forEach(field => {
let _value = options[field];
// convert xrp
if (_value && typeof _value === "object" && _value.$type === "xrp") {
if (+_value.$value) {
options[field] = (+_value.$value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (_value && typeof _value === "object" && _value.$type === "json") {
if (typeof _value.$value === "object") {
options[field] = _value.$value as any;
} else {
try {
options[field] = JSON.parse(_value.$value);
} catch (error) {
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
}`;
console.error(message)
options[field] = _value.$value
}
}
}
// delete unneccesary fields
if (options[field] === undefined) {
delete options[field];
}
});
return options
}
// editor value to state
export const prepareState = (value: string, txState: TransactionState) => {
const options = parseJSON(value);
if (!options) {
showAlert("Error!", {
body: "Cannot save editor with malformed transaction."
})
return
};
const { Account, TransactionType, Destination, ...rest } = options;
let tx: Partial<TransactionState> = {};
const { txFields } = txState
if (Account) {
const acc = state.accounts.find(acc => acc.address === Account);
if (acc) {
tx.selectedAccount = {
label: acc.name,
value: acc.address,
};
} else {
tx.selectedAccount = {
label: Account,
value: Account,
};
}
} else {
tx.selectedAccount = null;
}
if (TransactionType) {
tx.selectedTransaction = {
label: TransactionType,
value: TransactionType,
};
} else {
tx.selectedTransaction = null;
}
if (txFields.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination);
rest.Destination = null
if (dest) {
tx.selectedDestAccount = {
label: dest.name,
value: dest.address,
};
}
else if (Destination) {
tx.selectedDestAccount = {
label: Destination,
value: Destination,
};
}
else {
tx.selectedDestAccount = null
}
}
Object.keys(rest).forEach(field => {
const value = rest[field];
const origValue = txFields[field as keyof TxFields]
const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.$type === 'xrp'
if (isXrp) {
rest[field] = {
$type: "xrp",
$value: +value / 1000000, // TODO maybe use bigint?
};
} else if (typeof value === "object") {
rest[field] = {
$type: "json",
$value: value,
};
}
});
tx.txFields = rest;
tx.editorSavedValue = null;
return tx
}
export { transactionsData }

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),

View File

@@ -18,4 +18,14 @@ export const extractJSON = (str?: string) => {
} while (firstClose > firstOpen);
firstOpen = str.indexOf('{', firstOpen + 1);
} while (firstOpen != -1);
}
export const parseJSON = (str?: string | null): any | undefined => {
if (!str) return undefined
try {
const parsed = JSON.parse(str);
return typeof parsed === "object" ? parsed : undefined;
} catch (error) {
return undefined;
}
}

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';
}

39
utils/schema.ts Normal file
View File

@@ -0,0 +1,39 @@
export const extractSchemaProps = <O extends object>(obj: O) =>
Object.entries(obj).reduce((prev, [key, val]) => {
const typeOf = <T>(arg: T) =>
arg instanceof Array
? "array"
: arg === null
? "undefined"
: typeof arg;
const value = (typeOf(val) === "object" && '$type' in val && '$value' in val) ? val?.$value : val;
const type = typeOf(value);
let schema: any = {
title: key,
type,
default: value,
}
if (typeOf(value) === 'array') {
const item = value[0] // TODO merge other item schema's into one
if (typeOf(item) !== 'object') {
schema.items = {
type: 'object',
properties: extractSchemaProps(item),
default: item
}
}
// TODO support primitive-value arrays
}
if (typeOf(value) === "object") {
schema.properties = extractSchemaProps(value)
}
return {
...prev,
[key]: schema,
};
}, {} as any);

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).

1247
yarn.lock

File diff suppressed because it is too large Load Diff