Compare commits

..

26 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
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
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
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
28 changed files with 1292 additions and 507 deletions

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

@@ -50,15 +50,8 @@ import Stack from "./Stack";
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." };
}
@@ -225,8 +233,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<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>
@@ -529,8 +509,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
type="number"
min="1"
value={editorSettings.tabSize}
onChange={(e) =>
setEditorSettings((curr) => ({
onChange={e =>
setEditorSettings(curr => ({
...curr,
tabSize: Number(e.target.value),
}))

View File

@@ -27,6 +27,7 @@ const StyledContent = styled(PopoverPrimitive.Content, {
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)": {
@@ -43,7 +44,7 @@ const StyledContent = styled(PopoverPrimitive.Content, {
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
"0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)",
},
});
@@ -83,12 +84,15 @@ interface IPopover {
onOpenChange?: (open: boolean) => void;
}
const Popover: React.FC<IPopover> = ({
const Popover: React.FC<
IPopover & React.ComponentProps<typeof PopoverContent>
> = ({
children,
content,
open,
defaultOpen = false,
onOpenChange,
...rest
}) => (
<PopoverRoot
open={open}
@@ -96,8 +100,8 @@ const Popover: React.FC<IPopover> = ({
onOpenChange={onOpenChange}
>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent sideOffset={5}>
{content} <PopoverArrow />
<PopoverContent sideOffset={5} {...rest}>
{content} <PopoverArrow offset={5} className="arrow" />
</PopoverContent>
</PopoverRoot>
);

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

@@ -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

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

View File

@@ -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

@@ -7,7 +7,7 @@ export { default as Text } from "./Text";
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
}

View File

@@ -21,6 +21,7 @@
"@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",

View File

@@ -16,6 +16,7 @@ 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);
@@ -140,6 +141,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
})(),
}}
/>
<Alert />
</ThemeProvider>
</SessionProvider>
</IdProvider>

View File

@@ -1,4 +1,5 @@
import { Label } from "@radix-ui/react-label";
import { Switch, SwitchThumb } from "../../components/Switch";
import type { NextPage } from "next";
import dynamic from "next/dynamic";
import { Gear, Play } from "phosphor-react";
@@ -12,6 +13,7 @@ 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,
@@ -21,55 +23,177 @@ 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" }}>
<Label>Optimization level</Label>
<ButtonGroup css={{ mt: "$2", fontFamily: "$monospace" }}>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions !== "-O0"}
onClick={() => (state.compileOptions = "-O0")}
<Flex css={{ minWidth: 200, flexDirection: "column", gap: "$5" }}>
<Box>
<Label
style={{
flexDirection: "row",
display: "flex",
}}
>
-O0
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions !== "-O1"}
onClick={() => (state.compileOptions = "-O1")}
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",
}}
>
-O1
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions !== "-O2"}
onClick={() => (state.compileOptions = "-O2")}
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;
}}
>
-O2
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions !== "-O3"}
onClick={() => (state.compileOptions = "-O3")}
>
-O3
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions !== "-O4"}
onClick={() => (state.compileOptions = "-O4")}
>
-O4
</Button>
<Button
css={{ fontFamily: "$monospace" }}
outline={snap.compileOptions !== "-Os"}
onClick={() => (state.compileOptions = "-Os")}
>
-Os
</Button>
</ButtonGroup>
<SwitchThumb />
</Switch>
</Box>
</Flex>
);
};

View File

@@ -35,10 +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 || '-O0',
options: state.compileOptions.optimizationLevel || '-O0',
name: state.files[activeId].name,
src: state.files[activeId].content,
},

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

@@ -74,7 +74,10 @@ export interface IState {
mainModalOpen: boolean;
mainModalShowed: boolean;
accounts: IAccount[];
compileOptions: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
compileOptions: {
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
strip: boolean
}
}
// let localStorageState: null | string = null;
@@ -104,7 +107,10 @@ let initialState: IState = {
mainModalOpen: false,
mainModalShowed: false,
accounts: [],
compileOptions: '-O0'
compileOptions: {
optimizationLevel: '-O0',
strip: false
}
};
let localStorageAccounts: string | null = null;

View File

@@ -1,6 +1,32 @@
import { proxy } from 'valtio';
import { TransactionState } from '../components/Transaction';
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,
@@ -9,6 +35,8 @@ export const defaultTransaction: TransactionState = {
txIsLoading: false,
txIsDisabled: false,
txFields: {},
viewType: 'ui',
editorSavedValue: null
};
export const transactionsState = proxy({
@@ -24,11 +52,13 @@ export const transactionsState = proxy({
/**
* Simple transaction state changer
* @param header Unique key and tab name for the transaction tab
* @param partialTx partial transaction state, `{}` resets the state and `undefined` deletes the transaction
* @param partialTx partial transaction state, `undefined` deletes the transaction
*
*/
export const modifyTransaction = (
header: string,
partialTx?: Partial<TransactionState>
partialTx?: Partial<TransactionState>,
opts: { replaceState?: boolean } = {}
) => {
const tx = transactionsState.transactions.find(tx => tx.header === header);
@@ -40,14 +70,24 @@ export const modifyTransaction = (
}
if (!tx) {
const state = {
...defaultTransaction,
...partialTx,
}
transactionsState.transactions.push({
header,
state: {
...defaultTransaction,
...partialTx,
},
state,
});
return;
return state;
}
if (opts.replaceState) {
const repTx: TransactionState = {
...defaultTransaction,
...partialTx,
}
tx.state = repTx
return repTx
}
Object.keys(partialTx).forEach(k => {
@@ -56,4 +96,130 @@ export const modifyTransaction = (
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;
}
}

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

View File

@@ -674,7 +674,7 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "0.1.0"
"@radix-ui/react-label@^0.1.5":
"@radix-ui/react-label@0.1.5", "@radix-ui/react-label@^0.1.5":
version "0.1.5"
resolved "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-0.1.5.tgz"
integrity sha512-Au9+n4/DhvjR0IHhvZ1LPdx/OW+3CGDie30ZyCkbSHIuLp4/CV4oPPGBwJ1vY99Jog3zyQhsGww9MXj8O9Aj/A==
@@ -794,6 +794,21 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "0.1.0"
"@radix-ui/react-switch@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-0.1.5.tgz#071ffa19a17a47fdc5c5e6f371bd5901c9fef2f4"
integrity sha512-ITtslJPK+Yi34iNf7K9LtsPaLD76oRIVzn0E8JpEO5HW8gpRBGb2NNI9mxKtEB30TVqIcdjdL10AmuIfOMwjtg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.1.0"
"@radix-ui/react-compose-refs" "0.1.0"
"@radix-ui/react-context" "0.1.1"
"@radix-ui/react-label" "0.1.5"
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-use-controllable-state" "0.1.0"
"@radix-ui/react-use-previous" "0.1.1"
"@radix-ui/react-use-size" "0.1.1"
"@radix-ui/react-tooltip@^0.1.7":
version "0.1.7"
resolved "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-0.1.7.tgz"