Compare commits

...

74 Commits

Author SHA1 Message Date
muzam1l
b2b7059774 Remove account import limit. 2022-07-21 15:31:06 +05:30
muzamil
3897f2d823 Merge pull request #246 from XRPLF/feat/tabs-comp
Tabs and Context menu components.
2022-07-20 17:46:04 +05:30
muzam1l
bf792f1495 Fix dependecy issues. 2022-07-20 14:55:59 +05:30
muzam1l
df3210a663 Add context menu component 2022-07-20 14:12:48 +05:30
muzam1l
bad7730c32 Fix extension requirement error. 2022-07-20 14:05:02 +05:30
muzam1l
adb6a78549 update active compiled file based on active editor file. 2022-07-20 14:05:02 +05:30
muzam1l
8cc27f20c3 Migrate File navigation to tabs component. 2022-07-20 14:05:02 +05:30
muzamil
8e49a3f5f1 Merge pull request #253 from XRPLF/fix/json-tx
Fix json mode.
2022-07-20 13:59:42 +05:30
muzam1l
3179757469 Fix TxFields type. 2022-07-19 17:46:13 +05:30
muzam1l
554cfb3db9 Add fee field to all transactions. 2022-07-19 17:45:41 +05:30
muzam1l
637a066f69 Fix tx reset state button 2022-07-19 17:36:09 +05:30
muzam1l
c9a852e9be Add destination field to NFTokenCreateOffer 2022-07-19 16:05:10 +05:30
muzam1l
307a5407eb Fix handling of Destination field in transactions. 2022-07-19 16:01:21 +05:30
muzam1l
faa28845c8 Ensure editor value updation on state change 2022-07-18 21:26:05 +05:30
muzam1l
168d11d48e Remove editorSavedValue from tx state 2022-07-18 19:10:33 +05:30
muzam1l
60f2bb558c Cancel button on json discard dialog. 2022-07-18 17:10:13 +05:30
muzamil
fdf33b9f45 Merge pull request #251 from XRPLF/fix/binary-codec
Fix binary codec (#250)
2022-07-15 16:17:43 +05:30
muzam1l
d05180d148 Fix array fields. 2022-07-15 14:49:54 +05:30
muzamil
bfaa6be17d Fix binary codec (#250)
* TokenID -> NFTokenID

* update xrpl-accountlib

* fixup patch

* add missing transaction types

* fix: repositories

* fix: update @types/react, move middleware

* revert yarn.lock

* revert

* revert: yarn.lock

* fix: transactions

* update NFTokenCancelOffer

* add missing fee

* add: nft transaction type + recalculate `start`

Co-authored-by: dangell7 <denisangell7@gmail.com>
2022-07-15 13:34:19 +05:30
muzamil
9e368dec84 Merge pull request #248 from XRPLF/fix/trust-set
Fix TrustSet transaction example.
2022-07-15 13:13:21 +05:30
muzam1l
25eec6980f Fix TrustSet transaction example. 2022-07-14 20:05:32 +05:30
muzamil
8e2f20c5ac Merge pull request #239 from XRPLF/feat/monaco-comp
Monaco component.
2022-07-14 13:51:24 +05:30
muzam1l
a3d094e873 Fix some more spelling errors. 2022-07-13 20:11:21 +05:30
muzam1l
ef70bfb13a Fix spelling error 2022-07-13 19:49:54 +05:30
muzam1l
c26c7c13d1 Improve compile error handling. 2022-07-13 16:50:47 +05:30
muzam1l
fc461ddd0d Content changed warning on deploy page. 2022-07-13 16:17:31 +05:30
muzam1l
c7001f6089 'Exit editor mode' button in wat editor. 2022-07-13 15:40:08 +05:30
muzam1l
243cbfec08 Upgrade hooks editor to comp. 2022-07-13 15:09:45 +05:30
muzam1l
1295e7fa41 Deploy editor to comp 2022-07-13 15:09:45 +05:30
muzam1l
793623d216 Migrate transaction json editor to comp. 2022-07-13 15:09:45 +05:30
muzam1l
0cde0eb240 Monaco component 2022-07-13 15:09:45 +05:30
muzamil
e2acb48e03 Merge pull request #236 from XRPLF/feat/jsdoc-to-ui
User declaration of input fields via JSDOC in script files.
2022-07-12 16:53:27 +05:30
muzam1l
a4373bb970 fix spelling error 2022-07-12 12:40:35 +05:30
muzam1l
cfb791092a Update template files of some examples to forked versions. 2022-07-11 14:44:35 +05:30
muzamil
3fcbac5ed9 Merge pull request #231 from XRPLF/feat/create-account-name
Allow passing desired name while creating account.
2022-07-11 14:29:38 +05:30
muzam1l
c40b272ce8 Fix disabled prop behaviour 2022-07-07 23:34:14 +05:30
muzam1l
860ff66a8a fix error in error handler 2022-07-07 23:25:53 +05:30
muzam1l
f4f700bea1 Handle required prop on fields. 2022-07-06 19:30:25 +05:30
muzam1l
789bc00ac3 Enhance account secret label! 2022-07-06 19:03:29 +05:30
muzam1l
6a0aabdeda Handle jsdoc errors 2022-07-05 19:38:26 +05:30
muzam1l
175b6266e8 Add script error handling 2022-07-05 19:09:08 +05:30
muzam1l
621482e2ee enhance empty log display 2022-07-05 18:52:45 +05:30
muzam1l
e55f48bc83 Use Single LogBox comp for scripts too 2022-07-05 18:45:52 +05:30
muzam1l
3e9e26a46a Data variables in process.env instead of process 2022-07-05 16:58:07 +05:30
muzam1l
f0e730bb9b Remove secret tag on type Account 2022-07-05 16:35:23 +05:30
muzam1l
6ce4828fc6 Remove console logs 2022-07-05 16:25:15 +05:30
muzam1l
bb0a246ae5 User declaration of input fields via JSDOC in script files. 2022-07-05 16:02:15 +05:30
Valtteri Karesto
3af2bad536 Make ping interval longer (#232)
* Make ping interval 45s

Co-authored-by: Vaclav Barta <vbarta@mangrove.cz>
2022-07-04 12:22:06 +02:00
muzam1l
4f1b877db0 Added optional tag to create account label. 2022-07-01 19:41:52 +05:30
muzamil
0289d64f5e Merge pull request #233 from XRPLF/fix/tab-names
Fix tab names
2022-07-01 19:29:33 +05:30
muzamil
868a0bcf78 Merge pull request #234 from XRPLF/feat/account-in-deploy-dialog
Add account selectable in deploy dialog.
2022-07-01 18:51:04 +05:30
muzam1l
aab2476a05 Add account selectable in deploy dialog. 2022-07-01 18:06:01 +05:30
muzam1l
cb25986d72 update transaction tab labels 2022-07-01 17:30:57 +05:30
muzam1l
309ad57173 Skip auto appneding test file extension. 2022-07-01 17:26:10 +05:30
muzam1l
53afb1d3d1 Fix html erros. 2022-07-01 17:08:34 +05:30
muzam1l
31ff7c0e28 Name field in import account dialog. 2022-07-01 16:43:15 +05:30
muzam1l
dfa35df465 reset input value on submit 2022-07-01 16:27:13 +05:30
muzam1l
f163b052e1 Allow passing desired name while creating account. 2022-07-01 14:33:28 +05:30
Valtteri Karesto
25c5b9c015 Merge pull request #229 from XRPLF/feat/links-to-explorer
Link from hashes/addresses to Hook Explorer
2022-06-30 15:40:57 +03:00
Valtteri Karesto
407e3946ce Added underline on hover to links 2022-06-30 15:30:43 +03:00
Valtteri Karesto
dc5b0d71eb Simplified hook state, since endpoint now works with hookhashes 2022-06-30 08:54:43 +03:00
Valtteri Karesto
3fd6c3f50e Remove debug code 2022-06-29 18:03:26 +03:00
Valtteri Karesto
ec8bfc5eee Add links to account modal 2022-06-29 15:26:33 +03:00
Valtteri Karesto
b4a0bcb90d Merge pull request #227 from XRPLF/feat/remember-deploy-values
Remember deploy values / Add feedback button
2022-06-29 14:08:47 +03:00
Valtteri Karesto
2c729e2aa4 Update button text 2022-06-29 14:04:06 +03:00
Valtteri Karesto
1cb2542170 Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into feat/remember-deploy-values 2022-06-29 13:47:41 +03:00
Wietse Wind
00b309df34 Merge pull request #228 from XRPLF/feature/add-plausible-analytics
Add plausible analytics to builder
2022-06-29 12:10:58 +02:00
Joni Juup
a6fc730de6 add plausible analytics to builder 2022-06-29 12:58:17 +03:00
Valtteri Karesto
2245c5a221 Test gh integration 2022-06-29 12:18:38 +03:00
Valtteri Karesto
60c33661ad Add proper defaultvalue 2022-06-29 12:14:52 +03:00
Valtteri Karesto
ea21c85038 Add noopener and noreferrer to link 2022-06-29 11:37:22 +03:00
Valtteri Karesto
5478f43609 persist deploy values in memory 2022-06-29 11:32:15 +03:00
Valtteri Karesto
a9b64abb85 Add feedback button, show modal only on homepage 2022-06-29 11:31:46 +03:00
Valtteri Karesto
c6ced424d8 Merge pull request #226 from XRPLF/feat/long-navigation-support
Fix #215 scrollbar issues
2022-06-28 14:59:35 +03:00
43 changed files with 2240 additions and 1896 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@@ -0,0 +1,152 @@
import { CaretRight, Check, Circle } from "phosphor-react";
import { FC, Fragment, ReactNode } from "react";
import { Flex, Text } from "../";
import {
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuItemIndicator,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuRoot,
ContextMenuSeparator,
ContextMenuTrigger,
ContextMenuTriggerItem,
} from "./primitive";
[
{
label: "Show bookmarks",
type: "checkbox",
checked: true,
indicator: "*",
onCheckedChange: () => {},
},
{
type: "radio",
label: "People",
value: "pedro",
onValueChange: () => {},
options: [
{
value: "pedro",
label: "Pedro Duarte",
},
{
value: "colm",
label: "Colm Tuite",
},
],
},
];
export type TextOption = {
type: "text";
label: ReactNode;
onClick?: () => any;
children?: Option[];
};
export type SeparatorOption = { type: "separator" };
export type CheckboxOption = {
type: "checkbox";
label: ReactNode;
checked?: boolean;
onCheckedChange?: (isChecked: boolean) => any;
};
export type RadioOption<T extends string = string> = {
type: "radio";
label: ReactNode;
onValueChange?: (value: string) => any;
value: T;
options?: { value: T; label?: ReactNode }[];
};
type WithCommons = { key: string; disabled?: boolean };
type Option = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
WithCommons;
interface IContextMenu {
options?: Option[];
isNested?: boolean;
}
export const ContextMenu: FC<IContextMenu> = ({
children,
options,
isNested,
}) => {
return (
<ContextMenuRoot>
{isNested ? (
<ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
) : (
<ContextMenuTrigger>{children}</ContextMenuTrigger>
)}
{options && (
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
{options.map(({ key, ...option }) => {
if (option.type === "text") {
const { children, label } = option;
if (children)
return (
<ContextMenu isNested key={key} options={children}>
<Flex fluid row justify="space-between" align="center">
<Text>{label}</Text>
<CaretRight />
</Flex>
</ContextMenu>
);
return <ContextMenuItem key={key}>{label}</ContextMenuItem>;
}
if (option.type === "checkbox") {
const { label, checked, onCheckedChange } = option;
return (
<ContextMenuCheckboxItem
key={key}
checked={checked}
onCheckedChange={onCheckedChange}
>
<Flex row align="center">
<ContextMenuItemIndicator>
<Check />
</ContextMenuItemIndicator>
<Text css={{ ml: checked ? "$4" : undefined }}>
{label}
</Text>
</Flex>
</ContextMenuCheckboxItem>
);
}
if (option.type === "radio") {
const { label, options, onValueChange, value } = option;
return (
<Fragment key={key}>
<ContextMenuLabel>{label}</ContextMenuLabel>
<ContextMenuRadioGroup
value={value}
onValueChange={onValueChange}
>
{options?.map(({ value: v, label }) => {
return (
<ContextMenuRadioItem key={v} value={v}>
<ContextMenuItemIndicator>
<Circle weight="fill" />
</ContextMenuItemIndicator>
<Text css={{ ml: "$4" }}>{label}</Text>
</ContextMenuRadioItem>
);
})}
</ContextMenuRadioGroup>
</Fragment>
);
}
return <ContextMenuSeparator key={key} />;
})}
</ContextMenuContent>
)}
</ContextMenuRoot>
);
};
export default ContextMenu;

View File

@@ -0,0 +1,107 @@
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { styled } from "../../stitches.config";
import {
slideDownAndFade,
slideLeftAndFade,
slideRightAndFade,
slideUpAndFade,
} from "../../styles/keyframes";
const StyledContent = styled(ContextMenuPrimitive.Content, {
minWidth: 140,
backgroundColor: "$backgroundOverlay",
borderRadius: 6,
overflow: "hidden",
padding: "5px",
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
"@media (prefers-reduced-motion: no-preference)": {
animationDuration: "400ms",
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
willChange: "transform, opacity",
'&[data-state="open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
".dark &": {
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
},
});
const itemStyles = {
all: "unset",
fontSize: 13,
lineHeight: 1,
color: "$text",
borderRadius: 3,
display: "flex",
alignItems: "center",
height: 28,
padding: "0 7px",
position: "relative",
paddingLeft: 10,
userSelect: "none",
"&[data-disabled]": {
color: "$textMuted",
pointerEvents: "none",
},
"&:focus": {
backgroundColor: "$purple9",
color: "$white",
},
};
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles });
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
...itemStyles,
});
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
...itemStyles,
});
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
'&[data-state="open"]': {
backgroundColor: "$purple9",
color: "$purple9",
},
...itemStyles,
});
const StyledLabel = styled(ContextMenuPrimitive.Label, {
paddingLeft: 10,
fontSize: 12,
lineHeight: "25px",
color: "$text",
});
const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
height: 1,
backgroundColor: "$backgroundAlt",
margin: 5,
});
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, {
position: "absolute",
left: 0,
width: 25,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
});
export const ContextMenuRoot = ContextMenuPrimitive.Root;
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
export const ContextMenuContent = StyledContent;
export const ContextMenuItem = StyledItem;
export const ContextMenuCheckboxItem = StyledCheckboxItem;
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
export const ContextMenuRadioItem = StyledRadioItem;
export const ContextMenuItemIndicator = StyledItemIndicator;
export const ContextMenuTriggerItem = StyledTriggerItem;
export const ContextMenuLabel = StyledLabel;
export const ContextMenuSeparator = StyledSeparator;

View File

@@ -87,7 +87,7 @@ const addListeners = (account: ISelect | null) => {
if (streamState.socket) { if (streamState.socket) {
interval = setInterval(() => { interval = setInterval(() => {
streamState.socket?.send(""); streamState.socket?.send("");
}, 10000); }, 45000);
} }
streamState.socket.addEventListener("open", () => onOpen(account)); streamState.socket.addEventListener("open", () => onOpen(account));

View File

@@ -1,7 +1,6 @@
import React, { useRef, useState } from "react"; import React, { useState } from "react";
import { useSnapshot, ref } from "valtio"; import { useSnapshot } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import NextLink from "next/link"; import NextLink from "next/link";
@@ -10,33 +9,36 @@ import filesize from "filesize";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import state from "../state"; import state from "../state";
import wat from "../utils/wat-highlight"; import wat from "../utils/wat-highlight";
import EditorNavigation from "./EditorNavigation"; import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex } from "."; import { Button, Text, Link, Flex, Tabs, Tab } from ".";
import Monaco from "./Monaco";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]; const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
const DeployEditor = () => { const DeployEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state); const snap = useSnapshot(state);
const router = useRouter(); const router = useRouter();
const { theme } = useTheme(); const { theme } = useTheme();
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
const activeFile = snap.files[snap.active]?.compiledContent const compiledFiles = snap.files.filter(file => file.compiledContent);
? snap.files[snap.active] const activeFile = compiledFiles[snap.activeWat];
: snap.files.filter((file) => file.compiledContent)[0];
const renderNav = () => (
<Tabs
activeIndex={snap.activeWat}
onChangeActive={idx => (state.activeWat = idx)}
>
{compiledFiles.map((file, index) => {
return <Tab key={file.name} header={`${file.name}.wat`} />;
})}
</Tabs>
);
const compiledSize = activeFile?.compiledContent?.byteLength || 0; const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color = const color =
compiledSize > FILESIZE_BREAKPOINTS[1] compiledSize > FILESIZE_BREAKPOINTS[1]
@@ -45,6 +47,10 @@ const DeployEditor = () => {
? "$warning" ? "$warning"
: "$success"; : "$success";
const isContentChanged =
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
// const hasDeployErrors = activeFile && activeFile.containsErrors;
const CompiledStatView = activeFile && ( const CompiledStatView = activeFile && (
<Flex <Flex
column column
@@ -80,6 +86,12 @@ const DeployEditor = () => {
<Button variant="link" onClick={() => setShowContent(true)}> <Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file View as WAT-file
</Button> </Button>
{isContentChanged && (
<Text warning>
File contents were changed after last compile, compile again to
incorporate your latest changes in the build.
</Text>
)}
</Flex> </Flex>
); );
const NoContentView = !snap.loading && router.isReady && ( const NoContentView = !snap.loading && router.isReady && (
@@ -98,8 +110,9 @@ const DeployEditor = () => {
</NextLink> </NextLink>
</Text> </Text>
); );
const isContent = const isContent =
snap.files?.filter((file) => file.compiledWatContent).length > 0 && snap.files?.filter(file => file.compiledWatContent).length > 0 &&
router.isReady; router.isReady;
return ( return (
<Box <Box
@@ -112,7 +125,7 @@ const DeployEditor = () => {
width: "100%", width: "100%",
}} }}
> >
<EditorNavigation showWat /> <EditorNavigation renderNav={renderNav} />
<Container <Container
css={{ css={{
display: "flex", display: "flex",
@@ -126,32 +139,38 @@ const DeployEditor = () => {
) : !showContent ? ( ) : !showContent ? (
CompiledStatView CompiledStatView
) : ( ) : (
<Editor <Monaco
className="hooks-editor" className="hooks-editor"
defaultLanguage={"wat"} defaultLanguage={"wat"}
language={"wat"} language={"wat"}
path={`file://tmp/c/${activeFile?.name}.wat`} path={`file://tmp/c/${activeFile?.name}.wat`}
value={activeFile?.compiledWatContent || ""} value={activeFile?.compiledWatContent || ""}
beforeMount={(monaco) => { beforeMount={monaco => {
monaco.languages.register({ id: "wat" }); monaco.languages.register({ id: "wat" });
monaco.languages.setLanguageConfiguration("wat", wat.config); monaco.languages.setLanguageConfiguration("wat", wat.config);
monaco.languages.setMonarchTokensProvider("wat", wat.tokens); monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}} }}
onMount={(editor, monaco) => { onMount={editor => {
editorRef.current = editor;
editor.updateOptions({ editor.updateOptions({
glyphMargin: true, glyphMargin: true,
readOnly: true, readOnly: true,
}); });
}} }}
theme={theme === "dark" ? "dark" : "light"} theme={theme === "dark" ? "dark" : "light"}
overlay={
<Flex
css={{
m: "$1",
ml: "auto",
fontSize: "$sm",
color: "$textMuted",
}}
>
<Link onClick={() => setShowContent(false)}>
Exit editor mode
</Link>
</Flex>
}
/> />
)} )}
</Container> </Container>

View File

@@ -1,27 +1,7 @@
import { keyframes } from "@stitches/react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import { slideDownAndFade, slideLeftAndFade, slideRightAndFade, slideUpAndFade } from '../styles/keyframes';
const slideUpAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideRightAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(-2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const slideDownAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(-2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideLeftAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const StyledContent = styled(DropdownMenuPrimitive.Content, { const StyledContent = styled(DropdownMenuPrimitive.Content, {
minWidth: 220, minWidth: 220,

View File

@@ -1,6 +1,10 @@
import React, { useState, useEffect, useCallback, useRef } from "react"; import React, {
useState,
useEffect,
useRef,
ReactNode,
} from "react";
import { import {
Plus,
Share, Share,
DownloadSimple, DownloadSimple,
Gear, Gear,
@@ -28,7 +32,6 @@ import { useSnapshot } from "valtio";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { import {
createNewFile,
syncToGist, syncToGist,
updateEditorSettings, updateEditorSettings,
downloadAsZip, downloadAsZip,
@@ -48,36 +51,23 @@ import {
import Flex from "./Flex"; import Flex from "./Flex";
import Stack from "./Stack"; import Stack from "./Stack";
import { Input, Label } from "./Input"; import { Input, Label } from "./Input";
import Text from "./Text";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import { styled } from "../stitches.config";
import { showAlert } from "../state/actions/showAlert"; import { showAlert } from "../state/actions/showAlert";
const ErrorText = styled(Text, {
color: "$error",
mt: "$1",
display: "block",
});
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => { const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false); const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null);
const [filename, setFilename] = useState("");
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const [popup, setPopUp] = useState(false); const [popup, setPopUp] = useState(false);
const [editorSettings, setEditorSettings] = useState(snap.editorSettings); const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
useEffect(() => { useEffect(() => {
if (session && session.user && popup) { if (session && session.user && popup) {
setPopUp(false); setPopUp(false);
} }
}, [session, popup]); }, [session, popup]);
// when filename changes, reset error
useEffect(() => {
setNewfileError(null);
}, [filename, setNewfileError]);
const showNewGistAlert = () => { const showNewGistAlert = () => {
showAlert("Are you sure?", { showAlert("Are you sure?", {
@@ -95,46 +85,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
}); });
}; };
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)) {
return { error: "Filename already exists." };
}
if (!filename.includes(".") || filename[filename.length - 1] === ".") {
return { error: "Filename should include file extension" };
}
// check for illegal characters
const ALPHA_NUMERICAL_REGEX = /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g;
if (!filename.match(ALPHA_NUMERICAL_REGEX)) {
return {
error: `Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-" and it needs to have file extension (e.g. ".c")`,
};
}
return { error: null };
},
[snap.files]
);
const handleConfirm = useCallback(() => {
// add default extension in case omitted
const chk = validateFilename(filename);
if (chk && chk.error) {
setNewfileError(`Error: ${chk.error}`);
return;
}
setIsNewfileDialogOpen(false);
createNewFile(filename);
setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const files = snap.files;
return ( return (
<Flex css={{ flexShrink: 0, gap: "$0" }}> <Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex <Flex
@@ -174,131 +126,14 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
scrollbarWidth: "thin", scrollbarWidth: "thin",
}, },
}} }}
onWheelCapture={(e) => { onWheelCapture={e => {
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY; scrollRef.current.scrollLeft += e.deltaY;
} }
}} }}
> >
<Container css={{ flex: 1 }} ref={containerRef}> <Container css={{ flex: 1 }} ref={containerRef}>
<Stack {renderNav?.()}
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
}}
>
{files &&
files.length > 0 &&
files.map((file, index) => {
if (!file.compiledContent && showWat) {
return null;
}
return (
<Button
size="sm"
outline={
showWat ? snap.activeWat !== index : snap.active !== index
}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{file.name}
{showWat && ".wat"}
{!showWat && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active =
index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
);
})}
{!showWat && (
<Dialog
open={isNewfileDialogOpen}
onOpenChange={setIsNewfileDialogOpen}
>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<Label>Filename</Label>
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleConfirm();
}
}}
/>
<ErrorText>{newfileError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleConfirm}>
Create file
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
</Stack>
</Container> </Container>
</Flex> </Flex>
<Flex <Flex
@@ -542,8 +377,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
type="number" type="number"
min="1" min="1"
value={editorSettings.tabSize} value={editorSettings.tabSize}
onChange={(e) => onChange={e =>
setEditorSettings((curr) => ({ setEditorSettings(curr => ({
...curr, ...curr,
tabSize: Number(e.target.value), tabSize: Number(e.target.value),
})) }))

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio"; import { useSnapshot, ref } from "valtio";
import Editor from "@monaco-editor/react";
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
import { ArrowBendLeftUp } from "phosphor-react"; import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
@@ -8,9 +7,7 @@ import { useRouter } from "next/router";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import dark from "../theme/editor/amy.json"; import { createNewFile, saveFile } from "../state/actions";
import light from "../theme/editor/xcode_default.json";
import { saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants"; import { apiHeaderFiles } from "../state/constants";
import state from "../state"; import state from "../state";
@@ -22,10 +19,13 @@ import { listen } from "@codingame/monaco-jsonrpc";
import ReconnectingWebSocket from "reconnecting-websocket"; import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs"; import docs from "../xrpl-hooks-docs/docs";
import Monaco from "./Monaco";
import { saveAllFiles } from "../state/actions/saveFile";
import { Tab, Tabs } from "./Tabs";
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path; const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) { if (apiHeaderFiles.find(h => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true }); editor.updateOptions({ readOnly: true });
} else { } else {
editor.updateOptions({ readOnly: false }); editor.updateOptions({ readOnly: false });
@@ -42,7 +42,7 @@ const setMarkers = (monacoE: typeof monaco) => {
.getModelMarkers({}) .getModelMarkers({})
// Filter out the markers that are hooks specific // Filter out the markers that are hooks specific
.filter( .filter(
(marker) => marker =>
typeof marker?.code === "string" && typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-" // Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-") marker?.code?.includes("hooks-")
@@ -56,16 +56,16 @@ const setMarkers = (monacoE: typeof monaco) => {
// Add decoration (aka extra hoverMessages) to markers in the // Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are // exact same range (location) where the markers are
const models = monacoE.editor.getModels(); const models = monacoE.editor.getModels();
models.forEach((model) => { models.forEach(model => {
decorations[model.id] = model?.deltaDecorations( decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [], decorations?.[model.id] || [],
markers markers
.filter((marker) => .filter(marker =>
marker?.resource.path marker?.resource.path
.split("/") .split("/")
.includes(`${state.files?.[state.active]?.name}`) .includes(`${state.files?.[state.active]?.name}`)
) )
.map((marker) => ({ .map(marker => ({
range: new monacoE.Range( range: new monacoE.Range(
marker.startLineNumber, marker.startLineNumber,
marker.startColumn, marker.startColumn,
@@ -113,6 +113,33 @@ const HooksEditor = () => {
setMarkers(monacoRef.current); setMarkers(monacoRef.current);
} }
}, [snap.active]); }, [snap.active]);
useEffect(() => {
return () => {
saveAllFiles();
};
}, []);
const file = snap.files[snap.active];
const renderNav = () => (
<Tabs
label="File"
activeIndex={snap.active}
onChangeActive={idx => (state.active = idx)}
extensionRequired
onCreateNewTab={createNewFile}
onCloseTab={idx => state.files.splice(idx, 1)}
headerExtraValidation={{
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
error:
'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"',
}}
>
{snap.files.map((file, index) => {
return <Tab key={file.name} header={file.name} />;
})}
</Tabs>
);
return ( return (
<Box <Box
css={{ css={{
@@ -125,18 +152,18 @@ const HooksEditor = () => {
width: "100%", width: "100%",
}} }}
> >
<EditorNavigation /> <EditorNavigation renderNav={renderNav} />
{snap.files.length > 0 && router.isReady ? ( {snap.files.length > 0 && router.isReady ? (
<Editor <Monaco
className="hooks-editor"
keepCurrentModel keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language} defaultLanguage={file?.language}
language={snap.files?.[snap.active]?.language} language={file?.language}
path={`file:///work/c/${snap.files?.[snap.active]?.name}`} path={`file:///work/c/${file?.name}`}
defaultValue={snap.files?.[snap.active]?.content} defaultValue={file?.content}
beforeMount={(monaco) => { // onChange={val => (state.files[snap.active].content = val)} // Auto save?
beforeMount={monaco => {
if (!snap.editorCtx) { if (!snap.editorCtx) {
snap.files.forEach((file) => snap.files.forEach(file =>
monaco.editor.createModel( monaco.editor.createModel(
file.content, file.content,
file.language, file.language,
@@ -161,7 +188,7 @@ const HooksEditor = () => {
// listen when the web socket is opened // listen when the web socket is opened
listen({ listen({
webSocket: webSocket as WebSocket, webSocket: webSocket as WebSocket,
onConnection: (connection) => { onConnection: connection => {
// create and start the language client // create and start the language client
const languageClient = createLanguageClient(connection); const languageClient = createLanguageClient(connection);
const disposable = languageClient.start(); const disposable = languageClient.start();
@@ -177,7 +204,6 @@ const HooksEditor = () => {
}); });
} }
// // hook editor to global state
// editor.updateOptions({ // editor.updateOptions({
// minimap: { // minimap: {
// enabled: false, // enabled: false,
@@ -186,10 +212,6 @@ const HooksEditor = () => {
// }); // });
if (!state.editorCtx) { if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor); state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
} }
}} }}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
@@ -217,13 +239,13 @@ const HooksEditor = () => {
}); });
// Hacky way to hide Peek menu // Hacky way to hide Peek menu
editor.onContextMenu((e) => { editor.onContextMenu(e => {
const host = const host =
document.querySelector<HTMLElement>(".shadow-root-host"); document.querySelector<HTMLElement>(".shadow-root-host");
const contextMenuItems = const contextMenuItems =
host?.shadowRoot?.querySelectorAll("li.action-item"); host?.shadowRoot?.querySelectorAll("li.action-item");
contextMenuItems?.forEach((k) => { contextMenuItems?.forEach(k => {
// If menu item contains "Peek" lets hide it // If menu item contains "Peek" lets hide it
if (k.querySelector(".action-label")?.textContent === "Peek") { if (k.querySelector(".action-label")?.textContent === "Peek") {
// @ts-expect-error // @ts-expect-error

View File

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

View File

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

75
components/Monaco.tsx Normal file
View File

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

View File

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

View File

@@ -22,12 +22,12 @@ import {
import { TTS, tts } from "../utils/hookOnCalculator"; import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions"; import { deployHook } from "../state/actions";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../state"; import state, { IFile, SelectOption } from "../state";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook"; import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
import estimateFee from "../utils/estimateFee"; import estimateFee from "../utils/estimateFee";
const transactionOptions = Object.keys(tts).map((key) => ({ const transactionOptions = Object.keys(tts).map(key => ({
label: key, label: key,
value: key as keyof TTS, value: key as keyof TTS,
})); }));
@@ -56,9 +56,31 @@ export type SetHookData = {
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo( export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => { ({ accountAddress }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const account = snap.accounts.find((acc) => acc.address === accountAddress); const compiledFiles = snap.files.filter(file => file.compiledContent);
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false); const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState(
accountOptions.find(acc => acc.value === accountAddress)
);
const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value
);
const getHookNamespace = useCallback(
() =>
activeFile && snap.deployValues[activeFile.name]
? snap.deployValues[activeFile.name].HookNamespace
: activeFile?.name.split(".")[0] || "",
[activeFile, snap.deployValues]
);
const { const {
register, register,
handleSubmit, handleSubmit,
@@ -68,10 +90,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
getValues, getValues,
formState: { errors }, formState: { errors },
} = useForm<SetHookData>({ } = useForm<SetHookData>({
defaultValues: { defaultValues: (activeFile && snap.deployValues[activeFile.name]) || {
HookNamespace: HookNamespace: activeFile?.name.split(".")[0] || "",
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "", Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
Invoke: transactionOptions.filter((to) => to.label === "ttPAYMENT"),
}, },
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
@@ -81,14 +102,16 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const [formInitialized, setFormInitialized] = useState(false); const [formInitialized, setFormInitialized] = useState(false);
const [estimateLoading, setEstimateLoading] = useState(false); const [estimateLoading, setEstimateLoading] = useState(false);
const watchedFee = watch("Fee"); const watchedFee = watch("Fee");
// Update value if activeWat changes
// Update value if activeFile changes
useEffect(() => { useEffect(() => {
setValue( if (!activeFile) return;
"HookNamespace", const defaultValue = getHookNamespace();
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
); setValue("HookNamespace", defaultValue);
setFormInitialized(true); setFormInitialized(true);
}, [snap.activeWat, snap.files, setValue]); }, [setValue, activeFile, snap.deployValues, getHookNamespace]);
useEffect(() => { useEffect(() => {
if ( if (
watchedFee && watchedFee &&
@@ -106,19 +129,19 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
// name: "HookGrants", // unique name for your Field Array // name: "HookGrants", // unique name for your Field Array
// }); // });
const [hashedNamespace, setHashedNamespace] = useState(""); const [hashedNamespace, setHashedNamespace] = useState("");
const namespace = watch(
"HookNamespace", const namespace = watch("HookNamespace", getHookNamespace());
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
);
const calculateHashedValue = useCallback(async () => { const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace); const hashedVal = await sha256(namespace);
setHashedNamespace(hashedVal.toUpperCase()); setHashedNamespace(hashedVal.toUpperCase());
}, [namespace]); }, [namespace]);
useEffect(() => { useEffect(() => {
calculateHashedValue(); calculateHashedValue();
}, [namespace, calculateHashedValue]); }, [namespace, calculateHashedValue]);
// Calcucate initial fee estimate when modal opens // Calculate initial fee estimate when modal opens
useEffect(() => { useEffect(() => {
if (formInitialized && account) { if (formInitialized && account) {
(async () => { (async () => {
@@ -136,24 +159,18 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [formInitialized]); }, [formInitialized]);
if (!account) {
return null;
}
const tooLargeFile = () => { const tooLargeFile = () => {
const activeFile = snap.files[snap.active].compiledContent
? snap.files[snap.active]
: snap.files.filter((file) => file.compiledContent)[0];
return Boolean( return Boolean(
activeFile?.compiledContent?.byteLength && activeFile?.compiledContent?.byteLength &&
activeFile?.compiledContent?.byteLength >= 64000 activeFile?.compiledContent?.byteLength >= 64000
); );
}; };
const onSubmit: SubmitHandler<SetHookData> = async (data) => { const onSubmit: SubmitHandler<SetHookData> = async data => {
const currAccount = state.accounts.find( const currAccount = state.accounts.find(
(acc) => acc.address === account.address acc => acc.address === account?.address
); );
if (!account) return;
if (currAccount) currAccount.isLoading = true; if (currAccount) currAccount.isLoading = true;
const res = await deployHook(account, data); const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false; if (currAccount) currAccount.isLoading = false;
@@ -173,9 +190,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
uppercase uppercase
variant={"secondary"} variant={"secondary"}
disabled={ disabled={
account.isLoading || !account || account.isLoading || !activeFile || tooLargeFile()
!snap.files.filter((file) => file.compiledWatContent).length ||
tooLargeFile()
} }
> >
Set Hook Set Hook
@@ -186,14 +201,22 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<DialogTitle>Deploy configuration</DialogTitle> <DialogTitle>Deploy configuration</DialogTitle>
<DialogDescription as="div"> <DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}> <Stack css={{ width: "100%", flex: 1 }}>
<Box css={{ width: "100%" }}>
<Label>Account</Label>
<Select
instanceId="deploy-account"
placeholder="Select account"
hideSelectedOptions
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)}
/>
</Box>
<Box css={{ width: "100%" }}> <Box css={{ width: "100%" }}>
<Label>Invoke on transactions</Label> <Label>Invoke on transactions</Label>
<Controller <Controller
name="Invoke" name="Invoke"
control={control} control={control}
defaultValue={transactionOptions.filter(
(to) => to.label === "ttPAYMENT"
)}
render={({ field }) => ( render={({ field }) => (
<Select <Select
{...field} {...field}
@@ -210,9 +233,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Input <Input
{...register("HookNamespace", { required: true })} {...register("HookNamespace", { required: true })}
autoComplete={"off"} autoComplete={"off"}
defaultValue={
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
}
/> />
{errors.HookNamespace?.type === "required" && ( {errors.HookNamespace?.type === "required" && (
<Box css={{ display: "inline", color: "$red11" }}> <Box css={{ display: "inline", color: "$red11" }}>
@@ -275,7 +295,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
type="number" type="number"
{...register("Fee", { required: true })} {...register("Fee", { required: true })}
autoComplete={"off"} autoComplete={"off"}
onKeyPress={(e) => { onKeyPress={e => {
if (e.key === "." || e.key === ",") { if (e.key === "." || e.key === ",") {
e.preventDefault(); e.preventDefault();
} }
@@ -307,8 +327,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
alignContent: "center", alignContent: "center",
display: "flex", display: "flex",
}} }}
onClick={async (e) => { onClick={async e => {
e.preventDefault(); e.preventDefault();
if (!account) return;
setEstimateLoading(true); setEstimateLoading(true);
const formValues = getValues(); const formValues = getValues();
try { try {
@@ -407,7 +428,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
isLoading={account.isLoading} isLoading={account?.isLoading}
> >
Set Hook Set Hook
</Button> </Button>

View File

@@ -17,6 +17,8 @@ import {
} from "./Dialog"; } from "./Dialog";
import { Plus, X } from "phosphor-react"; import { Plus, X } from "phosphor-react";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import { capitalize } from "../utils/helpers";
import ContextMenu from "./ContextMenu";
const ErrorText = styled(Text, { const ErrorText = styled(Text, {
color: "$error", color: "$error",
@@ -25,19 +27,25 @@ const ErrorText = styled(Text, {
}); });
interface TabProps { interface TabProps {
header?: string; header: string;
children: ReactNode; children?: ReactNode;
} }
// TODO customise messages shown // TODO customize messages shown
interface Props { interface Props {
label?: string;
activeIndex?: number; activeIndex?: number;
activeHeader?: string; activeHeader?: string;
headless?: boolean; headless?: boolean;
children: ReactElement<TabProps>[]; children: ReactElement<TabProps>[];
keepAllAlive?: boolean; keepAllAlive?: boolean;
defaultExtension?: string; defaultExtension?: string;
forceDefaultExtension?: boolean; extensionRequired?: boolean;
allowedExtensions?: string[];
headerExtraValidation?: {
regex: string | RegExp;
error: string;
};
onCreateNewTab?: (name: string) => any; onCreateNewTab?: (name: string) => any;
onCloseTab?: (index: number, header?: string) => any; onCloseTab?: (index: number, header?: string) => any;
onChangeActive?: (index: number, header?: string) => any; onChangeActive?: (index: number, header?: string) => any;
@@ -46,6 +54,7 @@ interface Props {
export const Tab = (props: TabProps) => null; export const Tab = (props: TabProps) => null;
export const Tabs = ({ export const Tabs = ({
label = "Tab",
children, children,
activeIndex, activeIndex,
activeHeader, activeHeader,
@@ -54,8 +63,10 @@ export const Tabs = ({
onCreateNewTab, onCreateNewTab,
onCloseTab, onCloseTab,
onChangeActive, onChangeActive,
headerExtraValidation,
extensionRequired,
defaultExtension = "", defaultExtension = "",
forceDefaultExtension, allowedExtensions,
}: Props) => { }: Props) => {
const [active, setActive] = useState(activeIndex || 0); const [active, setActive] = useState(activeIndex || 0);
const tabs: TabProps[] = children.map(elem => elem.props); const tabs: TabProps[] = children.map(elem => elem.props);
@@ -83,12 +94,38 @@ export const Tabs = ({
const validateTabname = useCallback( const validateTabname = useCallback(
(tabname: string): { error: string | null } => { (tabname: string): { error: string | null } => {
if (!tabname) {
return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
}
if (tabs.find(tab => tab.header === tabname)) { if (tabs.find(tab => tab.header === tabname)) {
return { error: "Name already exists." }; return { error: `${capitalize(label)} name already exists.` };
}
const ext =
(tabname.includes(".") && tabname.split(".").pop()) ||
defaultExtension ||
"";
if (extensionRequired && !ext) {
return { error: "File extension is required!" };
}
if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: "This file extension is not allowed!" };
}
if (
headerExtraValidation &&
!tabname.match(headerExtraValidation.regex)
) {
return { error: headerExtraValidation.error };
} }
return { error: null }; return { error: null };
}, },
[tabs] [
allowedExtensions,
defaultExtension,
extensionRequired,
headerExtraValidation,
label,
tabs,
]
); );
const handleActiveChange = useCallback( const handleActiveChange = useCallback(
@@ -100,29 +137,25 @@ export const Tabs = ({
); );
const handleCreateTab = useCallback(() => { const handleCreateTab = useCallback(() => {
// add default extension in case omitted const chk = validateTabname(tabname);
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = _tabname + defaultExtension;
}
const chk = validateTabname(_tabname);
if (chk.error) { if (chk.error) {
setNewtabError(`Error: ${chk.error}`); setNewtabError(`Error: ${chk.error}`);
return; return;
} }
let _tabname = tabname;
if (defaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
}
setIsNewtabDialogOpen(false); setIsNewtabDialogOpen(false);
setTabname(""); setTabname("");
onCreateNewTab?.(_tabname); onCreateNewTab?.(_tabname);
// switch to new tab?
handleActiveChange(tabs.length, _tabname); handleActiveChange(tabs.length, _tabname);
}, [ }, [
tabname, tabname,
defaultExtension, defaultExtension,
forceDefaultExtension,
validateTabname, validateTabname,
onCreateNewTab, onCreateNewTab,
handleActiveChange, handleActiveChange,
@@ -136,8 +169,10 @@ export const Tabs = ({
} }
onCloseTab?.(idx, tabs[idx].header); onCloseTab?.(idx, tabs[idx].header);
handleActiveChange(idx, tabs[idx].header);
}, },
[active, onCloseTab, tabs] [active, handleActiveChange, onCloseTab, tabs]
); );
return ( return (
@@ -154,46 +189,47 @@ export const Tabs = ({
}} }}
> >
{tabs.map((tab, idx) => ( {tabs.map((tab, idx) => (
<Button <ContextMenu key={tab.header}>
key={tab.header} <Button
role="tab" role="tab"
tabIndex={idx} tabIndex={idx}
onClick={() => handleActiveChange(idx, tab.header)} onClick={() => handleActiveChange(idx, tab.header)}
onKeyPress={() => handleActiveChange(idx, tab.header)} onKeyPress={() => handleActiveChange(idx, tab.header)}
outline={active !== idx} outline={active !== idx}
size="sm" size="sm"
css={{ css={{
"&:hover": { "&:hover": {
span: { span: {
visibility: "visible", visibility: "visible",
},
},
}}
>
{tab.header || idx}
{onCloseTab && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
}, },
}} },
onClick={(ev: React.MouseEvent<HTMLElement>) => { }}
ev.stopPropagation(); >
handleCloseTab(idx); {tab.header || idx}
}} {onCloseTab && (
> <Box
<X size="9px" weight="bold" /> as="span"
</Box> css={{
)} display: "flex",
</Button> p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
handleCloseTab(idx);
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
</ContextMenu>
))} ))}
{onCreateNewTab && ( {onCreateNewTab && (
<Dialog <Dialog
@@ -206,13 +242,16 @@ export const Tabs = ({
size="sm" size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }} css={{ alignItems: "center", px: "$2", mr: "$3" }}
> >
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"} <Plus size="16px" />{" "}
{tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>Create new tab</DialogTitle> <DialogTitle>
Create new {label.toLocaleLowerCase()}
</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>Tabname</Label> <Label>{label} name</Label>
<Input <Input
value={tabname} value={tabname}
onChange={e => setTabname(e.target.value)} onChange={e => setTabname(e.target.value)}
@@ -249,29 +288,32 @@ export const Tabs = ({
)} )}
</Stack> </Stack>
)} )}
{keepAllAlive ? ( {keepAllAlive
tabs.map((tab, idx) => { ? tabs.map((tab, idx) => {
// TODO Maybe rule out fragments as children // TODO Maybe rule out fragments as children
if (!isValidElement(tab.children)) { if (!isValidElement(tab.children)) {
if (active !== idx) return null; if (active !== idx) return null;
return tab.children; return tab.children;
} }
let key = tab.children.key || tab.header || idx; let key = tab.children.key || tab.header || idx;
let { children } = tab; let { children } = tab;
let { style, ...props } = children.props; let { style, ...props } = children.props;
return ( return (
<children.type <children.type
key={key} key={key}
{...props} {...props}
style={{ ...style, display: active !== idx ? "none" : undefined }} style={{
/> ...style,
); display: active !== idx ? "none" : undefined,
}) }}
) : ( />
<Fragment key={tabs[active].header || active}> );
{tabs[active].children} })
</Fragment> : tabs[active] && (
)} <Fragment key={tabs[active].header || active}>
{tabs[active].children}
</Fragment>
)}
</> </>
); );
}; };

View File

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

View File

@@ -1,11 +1,14 @@
import { Play } from "phosphor-react"; import { Play } from "phosphor-react";
import { FC, useCallback, useEffect, useMemo } from "react"; import { FC, useCallback, useEffect } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../../state"; import state from "../../state";
import { import {
defaultTransactionType,
getTxFields,
modifyTransaction, modifyTransaction,
prepareState, prepareState,
prepareTransaction, prepareTransaction,
SelectOption,
TransactionState, TransactionState,
} from "../../state/transactions"; } from "../../state/transactions";
import { sendTransaction } from "../../state/actions"; import { sendTransaction } from "../../state/actions";
@@ -15,7 +18,7 @@ import Flex from "../Flex";
import { TxJson } from "./json"; import { TxJson } from "./json";
import { TxUI } from "./ui"; import { TxUI } from "./ui";
import { default as _estimateFee } from "../../utils/estimateFee"; import { default as _estimateFee } from "../../utils/estimateFee";
import toast from 'react-hot-toast'; import toast from "react-hot-toast";
export interface TransactionProps { export interface TransactionProps {
header: string; header: string;
@@ -34,7 +37,6 @@ const Transaction: FC<TransactionProps> = ({
txIsDisabled, txIsDisabled,
txIsLoading, txIsLoading,
viewType, viewType,
editorSavedValue,
editorValue, editorValue,
} = txState; } = txState;
@@ -46,7 +48,7 @@ const Transaction: FC<TransactionProps> = ({
); );
const prepareOptions = useCallback( const prepareOptions = useCallback(
(state: TransactionState = txState) => { (state: Partial<TransactionState> = txState) => {
const { const {
selectedTransaction, selectedTransaction,
selectedDestAccount, selectedDestAccount,
@@ -55,9 +57,7 @@ const Transaction: FC<TransactionProps> = ({
} = state; } = state;
const TransactionType = selectedTransaction?.value || null; const TransactionType = selectedTransaction?.value || null;
const Destination = const Destination = selectedDestAccount?.value || txFields?.Destination;
selectedDestAccount?.value ||
("Destination" in txFields ? null : undefined);
const Account = selectedAccount?.value || null; const Account = selectedAccount?.value || null;
return prepareTransaction({ return prepareTransaction({
@@ -109,8 +109,9 @@ const Transaction: FC<TransactionProps> = ({
} }
const options = prepareOptions(st); const options = prepareOptions(st);
if (options.Destination === null) { const fields = getTxFields(options.TransactionType);
throw Error("Destination account cannot be null"); if (fields.Destination && !options.Destination) {
throw Error("Destination account is required!");
} }
await sendTransaction(account, options, { logPrefix }); await sendTransaction(account, options, { logPrefix });
@@ -136,15 +137,38 @@ const Transaction: FC<TransactionProps> = ({
prepareOptions, prepareOptions,
]); ]);
const resetState = useCallback(() => { const getJsonString = useCallback(
modifyTransaction(header, { viewType }, { replaceState: true }); (state?: Partial<TransactionState>) =>
}, [header, viewType]); JSON.stringify(
prepareOptions?.(state) || {},
null,
editorSettings.tabSize
),
[editorSettings.tabSize, prepareOptions]
);
const jsonValue = useMemo( const resetState = useCallback(
() => (transactionType: SelectOption | undefined = defaultTransactionType) => {
editorSavedValue || const fields = getTxFields(transactionType?.value);
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize),
[editorSavedValue, editorSettings.tabSize, prepareOptions] const nwState: Partial<TransactionState> = {
viewType,
selectedTransaction: transactionType,
};
if (fields.Destination !== undefined) {
nwState.selectedDestAccount = null;
fields.Destination = "";
} else {
fields.Destination = undefined;
}
nwState.txFields = fields;
const state = modifyTransaction(header, nwState, { replaceState: true });
const editorValue = getJsonString(state);
return setState({ editorValue });
},
[getJsonString, header, setState, viewType]
); );
const estimateFee = useCallback( const estimateFee = useCallback(
@@ -156,10 +180,10 @@ const Transaction: FC<TransactionProps> = ({
); );
if (!account) { if (!account) {
if (!opts?.silent) { if (!opts?.silent) {
toast.error("Please select account from the list.") toast.error("Please select account from the list.");
} }
return return;
}; }
ptx.Account = account.address; ptx.Account = account.address;
ptx.Sequence = account.sequence; ptx.Sequence = account.sequence;
@@ -176,7 +200,7 @@ const Transaction: FC<TransactionProps> = ({
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}> <Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
{viewType === "json" ? ( {viewType === "json" ? (
<TxJson <TxJson
value={jsonValue} getJsonString={getJsonString}
header={header} header={header}
state={txState} state={txState}
setState={setState} setState={setState}
@@ -199,7 +223,7 @@ const Transaction: FC<TransactionProps> = ({
<Button <Button
onClick={() => { onClick={() => {
if (viewType === "ui") { if (viewType === "ui") {
setState({ editorSavedValue: null, viewType: "json" }); setState({ viewType: "json" });
} else setState({ viewType: "ui" }); } else setState({ viewType: "ui" });
}} }}
outline outline
@@ -207,7 +231,7 @@ const Transaction: FC<TransactionProps> = ({
{viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"} {viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"}
</Button> </Button>
<Flex row> <Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}> <Button onClick={() => resetState()} outline css={{ mr: "$3" }}>
RESET RESET
</Button> </Button>
<Button <Button

View File

@@ -1,9 +1,4 @@
import Editor, { loader, useMonaco } from "@monaco-editor/react"; import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { FC, useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import dark from "../../theme/editor/amy.json";
import light from "../../theme/editor/xcode_default.json";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state, { import state, {
prepareState, prepareState,
@@ -11,21 +6,16 @@ import state, {
TransactionState, TransactionState,
} from "../../state"; } from "../../state";
import Text from "../Text"; import Text from "../Text";
import Flex from "../Flex"; import { Flex, Link } from "..";
import { Link } from "..";
import { showAlert } from "../../state/actions/showAlert"; import { showAlert } from "../../state/actions/showAlert";
import { parseJSON } from "../../utils/json"; import { parseJSON } from "../../utils/json";
import { extractSchemaProps } from "../../utils/schema"; import { extractSchemaProps } from "../../utils/schema";
import amountSchema from "../../content/amount-schema.json"; import amountSchema from "../../content/amount-schema.json";
import Monaco from "../Monaco";
loader.config({ import type monaco from "monaco-editor";
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
interface JsonProps { interface JsonProps {
value?: string; getJsonString?: (state?: Partial<TransactionState>) => string;
header?: string; header?: string;
setState: (pTx?: Partial<TransactionState> | undefined) => void; setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState; state: TransactionState;
@@ -33,23 +23,23 @@ interface JsonProps {
} }
export const TxJson: FC<JsonProps> = ({ export const TxJson: FC<JsonProps> = ({
value = "", getJsonString,
state: txState, state: txState,
header, header,
setState, setState,
}) => { }) => {
const { editorSettings, accounts } = useSnapshot(state); const { editorSettings, accounts } = useSnapshot(state);
const { editorValue = value, estimatedFee } = txState; const { editorValue, estimatedFee } = txState;
const { theme } = useTheme();
const [hasUnsaved, setHasUnsaved] = useState(false);
const [currTxType, setCurrTxType] = useState<string | undefined>( const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value txState.selectedTransaction?.value
); );
useEffect(() => { useEffect(() => {
setState({ editorValue: value }); setState({
editorValue: getJsonString?.(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]); }, []);
useEffect(() => { useEffect(() => {
const parsed = parseJSON(editorValue); const parsed = parseJSON(editorValue);
@@ -63,21 +53,22 @@ export const TxJson: FC<JsonProps> = ({
} }
}, [editorValue]); }, [editorValue]);
useEffect(() => {
if (editorValue === value) setHasUnsaved(false);
else setHasUnsaved(true);
}, [editorValue, value]);
const saveState = (value: string, transactionType?: string) => { const saveState = (value: string, transactionType?: string) => {
const tx = prepareState(value, transactionType); const tx = prepareState(value, transactionType);
if (tx) setState(tx); if (tx) {
setState(tx);
setState({
editorValue: getJsonString?.(tx),
});
}
}; };
const discardChanges = () => { const discardChanges = () => {
showAlert("Confirm", { showAlert("Confirm", {
body: "Are you sure to discard these changes?", body: "Are you sure to discard these changes?",
confirmText: "Yes", confirmText: "Yes",
onConfirm: () => setState({ editorValue: value }), onCancel: () => {},
onConfirm: () => setState({ editorValue: getJsonString?.() }),
}); });
}; };
@@ -90,14 +81,11 @@ export const TxJson: FC<JsonProps> = ({
showAlert("Error!", { showAlert("Error!", {
body: `Malformed Transaction in ${header}, would you like to discard these changes?`, body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
confirmText: "Discard", confirmText: "Discard",
onConfirm: () => setState({ editorValue: value }), onConfirm: () => setState({ editorValue: getJsonString?.() }),
onCancel: () => setState({ viewType: "json", editorSavedValue: value }), onCancel: () => setState({ viewType: "json" }),
}); });
}; };
const path = `file:///${header}`;
const monaco = useMonaco();
const getSchemas = useCallback(async (): Promise<any[]> => { const getSchemas = useCallback(async (): Promise<any[]> => {
const txObj = transactionsData.find( const txObj = transactionsData.find(
td => td.TransactionType === currTxType td => td.TransactionType === currTxType
@@ -177,55 +165,68 @@ export const TxJson: FC<JsonProps> = ({
]; ];
}, [accounts, currTxType, estimatedFee, header]); }, [accounts, currTxType, estimatedFee, header]);
const [monacoInst, setMonacoInst] = useState<typeof monaco>();
useEffect(() => { useEffect(() => {
if (!monaco) return; if (!monacoInst) return;
getSchemas().then(schemas => { getSchemas().then(schemas => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true, validate: true,
schemas, schemas,
}); });
}); });
}, [getSchemas, monaco]); }, [getSchemas, monacoInst]);
const hasUnsaved = useMemo(
() => editorValue !== getJsonString?.(),
[editorValue, getJsonString]
);
return ( return (
<Flex <Monaco
fluid rootProps={{
column css: { height: "calc(100% - 45px)" },
css={{ height: "calc(100% - 45px)", position: "relative" }} }}
> language={"json"}
<Editor id={header}
className="hooks-editor" height="100%"
language={"json"} value={editorValue}
path={path} onChange={val => setState({ editorValue: val })}
height="100%" onMount={(editor, monaco) => {
beforeMount={monaco => { editor.updateOptions({
monaco.editor.defineTheme("dark", dark as any); minimap: { enabled: false },
monaco.editor.defineTheme("light", light as any); glyphMargin: true,
}} tabSize: editorSettings.tabSize,
value={editorValue} dragAndDrop: true,
onChange={val => setState({ editorValue: val })} fontSize: 14,
onMount={(editor, monaco) => { });
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
});
// register onExit cb setMonacoInst(monaco);
const model = editor.getModel(); // register onExit cb
model?.onWillDispose(() => onExit(model.getValue())); const model = editor.getModel();
}} model?.onWillDispose(() => onExit(model.getValue()));
theme={theme === "dark" ? "dark" : "light"} }}
/> overlay={
{hasUnsaved && ( hasUnsaved ? (
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}> <Flex
This file has unsaved changes.{" "} row
<Link onClick={() => saveState(editorValue, currTxType)}>save</Link>{" "} align="center"
<Link onClick={discardChanges}>discard</Link> css={{ fontSize: "$xs", color: "$textMuted", ml: "auto" }}
</Text> >
)} <Text muted small>
</Flex> This file has unsaved changes.
</Text>
<Link
css={{ ml: "$1" }}
onClick={() => saveState(editorValue || "", currTxType)}
>
save
</Link>
<Link css={{ ml: "$1" }} onClick={discardChanges}>
discard
</Link>
</Flex>
) : undefined
}
/>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { FC, useCallback, useEffect, useState } from "react"; import { FC, useCallback, useEffect, useMemo, useState } from "react";
import Container from "../Container"; import Container from "../Container";
import Flex from "../Flex"; import Flex from "../Flex";
import Input from "../Input"; import Input from "../Input";
@@ -7,9 +7,10 @@ import Text from "../Text";
import { import {
SelectOption, SelectOption,
TransactionState, TransactionState,
transactionsData, transactionsOptions,
TxFields, TxFields,
getTxFields, getTxFields,
defaultTransactionType,
} from "../../state/transactions"; } from "../../state/transactions";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../../state"; import state from "../../state";
@@ -38,29 +39,30 @@ export const TxUI: FC<UIProps> = ({
txFields, txFields,
} = txState; } = txState;
const transactionsOptions = transactionsData.map((tx) => ({ const accountOptions: SelectOption[] = accounts.map(acc => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const accountOptions: SelectOption[] = accounts.map((acc) => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
})); }));
const destAccountOptions: SelectOption[] = accounts const destAccountOptions: SelectOption[] = accounts
.map((acc) => ({ .map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
})) }))
.filter((acc) => acc.value !== selectedAccount?.value); .filter(acc => acc.value !== selectedAccount?.value);
const [feeLoading, setFeeLoading] = useState(false); const [feeLoading, setFeeLoading] = useState(false);
const resetOptions = useCallback( const resetFields = useCallback(
(tt: string) => { (tt: string) => {
const fields = getTxFields(tt); const fields = getTxFields(tt);
if (!fields.Destination) setState({ selectedDestAccount: null });
if (fields.Destination !== undefined) {
setState({ selectedDestAccount: null });
fields.Destination = "";
} else {
fields.Destination = undefined;
}
return setState({ txFields: fields }); return setState({ txFields: fields });
}, },
[setState] [setState]
@@ -97,33 +99,42 @@ export const TxUI: FC<UIProps> = ({
[estimateFee, handleSetField] [estimateFee, handleSetField]
); );
const handleChangeTxType = (tt: SelectOption) => { const handleChangeTxType = useCallback(
setState({ selectedTransaction: tt }); (tt: SelectOption) => {
setState({ selectedTransaction: tt });
const newState = resetOptions(tt.value); const newState = resetFields(tt.value);
handleEstimateFee(newState, true); handleEstimateFee(newState, true);
}; },
[handleEstimateFee, resetFields, setState]
);
const specialFields = ["TransactionType", "Account", "Destination"]; const switchToJson = () => setState({ viewType: "json" });
// default tx
useEffect(() => {
if (selectedTransaction?.value) return;
if (defaultTransactionType) {
handleChangeTxType(defaultTransactionType);
}
}, [handleChangeTxType, selectedTransaction?.value]);
const fields = useMemo(
() => getTxFields(selectedTransaction?.value),
[selectedTransaction?.value]
);
const specialFields = ["TransactionType", "Account"];
if (fields.Destination !== undefined) {
specialFields.push("Destination");
}
const otherFields = Object.keys(txFields).filter( const otherFields = Object.keys(txFields).filter(
(k) => !specialFields.includes(k) k => !specialFields.includes(k)
) as [keyof TxFields]; ) as [keyof TxFields];
const switchToJson = () =>
setState({ editorSavedValue: null, viewType: "json" });
useEffect(() => {
const defaultOption = transactionsOptions.find(
(tt) => tt.value === "Payment"
);
if (defaultOption) {
handleChangeTxType(defaultOption);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<Container <Container
css={{ css={{
@@ -179,7 +190,7 @@ export const TxUI: FC<UIProps> = ({
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/> />
</Flex> </Flex>
{txFields.Destination !== undefined && ( {fields.Destination !== undefined && (
<Flex <Flex
row row
fluid fluid
@@ -204,7 +215,7 @@ export const TxUI: FC<UIProps> = ({
/> />
</Flex> </Flex>
)} )}
{otherFields.map((field) => { {otherFields.map(field => {
let _value = txFields[field]; let _value = txFields[field];
let value: string | undefined; let value: string | undefined;
@@ -255,7 +266,7 @@ export const TxUI: FC<UIProps> = ({
<Input <Input
type={isFee ? "number" : "text"} type={isFee ? "number" : "text"}
value={value} value={value}
onChange={(e) => { onChange={e => {
if (isFee) { if (isFee) {
const val = e.target.value const val = e.target.value
.replaceAll(".", "") .replaceAll(".", "")
@@ -267,7 +278,7 @@ export const TxUI: FC<UIProps> = ({
}} }}
onKeyPress={ onKeyPress={
isFee isFee
? (e) => { ? e => {
if (e.key === "." || e.key === ",") { if (e.key === "." || e.key === ",") {
e.preventDefault(); e.preventDefault();
} }

View File

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

View File

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

View File

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

View File

@@ -16,8 +16,9 @@
"@octokit/core": "^3.5.1", "@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7", "@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1", "@radix-ui/react-alert-dialog": "^0.1.1",
"@radix-ui/react-context-menu": "^0.1.6",
"@radix-ui/react-dialog": "^0.1.1", "@radix-ui/react-dialog": "^0.1.1",
"@radix-ui/react-dropdown-menu": "^0.1.1", "@radix-ui/react-dropdown-menu": "^0.1.6",
"@radix-ui/react-id": "^0.1.1", "@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5", "@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6", "@radix-ui/react-popover": "^0.1.6",
@@ -25,17 +26,18 @@
"@radix-ui/react-tooltip": "^0.1.7", "@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.8",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"comment-parser": "^1.3.1",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"filesize": "^8.0.7", "filesize": "^8.0.7",
"handlebars": "^4.7.7",
"javascript-time-ago": "^2.3.11", "javascript-time-ago": "^2.3.11",
"jszip": "^3.7.1", "jszip": "^3.7.1",
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
"lodash.xor": "^4.5.0", "lodash.xor": "^4.5.0",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"next": "^12.0.4", "next": "^12.0.4",
"next-auth": "^4.0.0-beta.5", "next-auth": "^4.10.1",
"next-plausible": "^3.2.0",
"next-themes": "^0.1.1", "next-themes": "^0.1.1",
"normalize-url": "^7.0.2", "normalize-url": "^7.0.2",
"octokit": "^1.7.0", "octokit": "^1.7.0",
@@ -60,7 +62,7 @@
"vscode-languageserver": "^7.0.0", "vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2", "vscode-uri": "^3.0.2",
"wabt": "1.0.16", "wabt": "1.0.16",
"xrpl-accountlib": "^1.3.2", "xrpl-accountlib": "^1.5.2",
"xrpl-client": "^1.9.4" "xrpl-client": "^1.9.4"
}, },
"devDependencies": { "devDependencies": {
@@ -74,5 +76,8 @@
"eslint-config-next": "11.1.2", "eslint-config-next": "11.1.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"typescript": "4.4.4" "typescript": "4.4.4"
},
"resolutions": {
"ripple-binary-codec": "=1.4.2"
} }
} }

View File

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

View File

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

View File

@@ -6,8 +6,9 @@ import Transaction from "../../components/Transaction";
import state from "../../state"; import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTransaction } from "../../state"; import { transactionsState, modifyTransaction } from "../../state";
import LogBoxForScripts from "../../components/LogBoxForScripts";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FileJs } from "phosphor-react";
import RunScript from '../../components/RunScript';
const DebugStream = dynamic(() => import("../../components/DebugStream"), { const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false, ssr: false,
@@ -33,7 +34,17 @@ const Test = () => {
return null; return null;
} }
const hasScripts = Boolean( const hasScripts = Boolean(
snap.files.filter((f) => f.name.toLowerCase()?.endsWith(".js")).length snap.files.filter(f => f.name.toLowerCase()?.endsWith(".js")).length
);
const renderNav = () => (
<Flex css={{ gap: "$3" }}>
{snap.files
.filter(f => f.name.endsWith(".js"))
.map(file => (
<RunScript file={file} key={file.name} />
))}
</Flex>
); );
return ( return (
@@ -50,7 +61,7 @@ const Test = () => {
gutterSize={4} gutterSize={4}
gutterAlign="center" gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }} style={{ height: "calc(100vh - 60px)" }}
onDragEnd={(e) => saveSplit("testVertical", e)} onDragEnd={e => saveSplit("testVertical", e)}
> >
<Flex <Flex
row row
@@ -72,19 +83,20 @@ const Test = () => {
width: "100%", width: "100%",
height: "100%", height: "100%",
}} }}
onDragEnd={(e) => saveSplit("testHorizontal", e)} onDragEnd={e => saveSplit("testHorizontal", e)}
> >
<Box css={{ width: "55%", px: "$2" }}> <Box css={{ width: "55%", px: "$2" }}>
<Tabs <Tabs
label="Transaction"
activeHeader={activeHeader} activeHeader={activeHeader}
// TODO make header a required field // TODO make header a required field
onChangeActive={(idx, header) => { onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header; if (header) transactionsState.activeHeader = header;
}} }}
keepAllAlive keepAllAlive
forceDefaultExtension defaultExtension="json"
defaultExtension=".json" allowedExtensions={["json"]}
onCreateNewTab={(header) => modifyTransaction(header, {})} onCreateNewTab={header => modifyTransaction(header, {})}
onCloseTab={(idx, header) => onCloseTab={(idx, header) =>
header && modifyTransaction(header, undefined) header && modifyTransaction(header, undefined)
} }
@@ -110,10 +122,12 @@ const Test = () => {
flexDirection: "column", flexDirection: "column",
}} }}
> >
<LogBoxForScripts <LogBox
Icon={FileJs}
title="Helper scripts" title="Helper scripts"
logs={snap.scriptLogs} logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])} clearLog={() => (state.scriptLogs = [])}
renderNav={renderNav}
/> />
</Flex> </Flex>
) : null} ) : null}

View File

@@ -27,41 +27,35 @@ export const names = [
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint * new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
* is protected with CORS so that's why we did our own endpoint * is protected with CORS so that's why we did our own endpoint
*/ */
export const addFaucetAccount = async (showToast: boolean = false) => { export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
// Lets limit the number of faucet accounts to 5 for now if (typeof window === undefined) return
if (state.accounts.length > 5) {
return toast.error("You can only have maximum 6 accounts");
}
if (typeof window !== 'undefined') {
const toastId = showToast ? toast.loading("Creating account") : "";
const toastId = showToast ? toast.loading("Creating account") : ""; const res = await fetch(`${window.location.origin}/api/faucet`, {
const res = await fetch(`${window.location.origin}/api/faucet`, { method: "POST",
method: "POST", });
}); const json: FaucetAccountRes | { error: string } = await res.json();
const json: FaucetAccountRes | { error: string } = await res.json(); if ("error" in json) {
if ("error" in json) { if (showToast) {
if (showToast) { return toast.error(json.error, { id: toastId });
return toast.error(json.error, { id: toastId });
} else {
return;
}
} else { } else {
if (showToast) { return;
toast.success("New account created", { id: toastId });
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
name: names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
} }
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
} }
}; };

View File

@@ -14,19 +14,21 @@ import { ref } from "valtio";
*/ */
export const compileCode = async (activeId: number) => { export const compileCode = async (activeId: number) => {
// Save the file to global state // Save the file to global state
saveFile(false); saveFile(false, activeId);
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) { if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error("Missing env!"); throw Error("Missing env!");
} }
// Bail out if we're already compiling // Bail out if we're already compiling
if (state.compiling) { if (state.compiling) {
// if compiling is ongoing return // if compiling is ongoing return // TODO Inform user about it.
return; return;
} }
// Set loading state to true // Set loading state to true
state.compiling = true; state.compiling = true;
state.logs = [] state.logs = []
const file = state.files[activeId]
try { try {
file.containsErrors = false
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, { const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: {
@@ -40,8 +42,8 @@ export const compileCode = async (activeId: number) => {
{ {
type: "c", type: "c",
options: state.compileOptions.optimizationLevel || '-O2', options: state.compileOptions.optimizationLevel || '-O2',
name: state.files[activeId].name, name: file.name,
src: state.files[activeId].content, src: file.content,
}, },
], ],
}), }),
@@ -49,15 +51,15 @@ export const compileCode = async (activeId: number) => {
const json = await res.json(); const json = await res.json();
state.compiling = false; state.compiling = false;
if (!json.success) { if (!json.success) {
state.logs.push({ type: "error", message: json.message }); const errors = [json.message]
if (json.tasks && json.tasks.length > 0) { if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => { json.tasks.forEach((task: any) => {
if (!task.success) { if (!task.success) {
state.logs.push({ type: "error", message: task?.console }); errors.push(task?.console)
} }
}); });
} }
return toast.error(`Couldn't compile!`, { position: "bottom-center" }); throw errors
} }
state.logs.push({ state.logs.push({
type: "success", type: "success",
@@ -67,8 +69,9 @@ export const compileCode = async (activeId: number) => {
}); });
// Decode base64 encoded wasm that is coming back from the endpoint // Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output); const bufferData = await decodeBinary(json.output);
state.files[state.active].compiledContent = ref(bufferData); file.compiledContent = ref(bufferData);
state.files[state.active].lastCompiled = new Date(); file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content
// Import wabt from and create human readable version of wasm file and // Import wabt from and create human readable version of wasm file and
// put it into state // put it into state
import("wabt").then((wabt) => { import("wabt").then((wabt) => {
@@ -84,10 +87,23 @@ export const compileCode = async (activeId: number) => {
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
state.logs.push({
type: "error", if (err instanceof Array && typeof err[0] === 'string') {
message: "Error occured while compiling!", err.forEach(message => {
}); state.logs.push({
type: "error",
message,
});
})
}
else {
state.logs.push({
type: "error",
message: "Something went wrong, check your connection try again later!",
});
}
state.compiling = false; state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,13 +13,13 @@ export const templateFileIds = {
}, },
'firewall': { 'firewall': {
id: '741816f53eddac862ef1ba400e1b9b84', id: '1cc30f39c8a0b9c55b88c312669ca45e', // Forked
name: 'Firewall', name: 'Firewall',
description: 'This Hook essentially checks a blacklist of accounts', description: 'This Hook essentially checks a blacklist of accounts',
icon: Firewall icon: Firewall
}, },
'notary': { 'notary': {
id: '0dfe12adb0aa75cff24c3c19497fb95a', id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked
name: 'Notary', name: 'Notary',
description: 'Collecting signatures for multi-sign transactions', description: 'Collecting signatures for multi-sign transactions',
icon: Notary icon: Notary
@@ -31,7 +31,7 @@ export const templateFileIds = {
icon: Carbon icon: Carbon
}, },
'peggy': { 'peggy': {
id: '52e61c02e777c44c913808981a4ca61f', id: '049784a83fa068faf7912f663f7b6471', // Forked
name: 'Peggy', name: 'Peggy',
description: 'An oracle based stable coin hook', description: 'An oracle based stable coin hook',
icon: Peggy icon: Peggy

View File

@@ -1,6 +1,6 @@
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
import { proxy, ref, subscribe } from "valtio"; import { proxy, ref, subscribe } from "valtio";
import { devtools } from 'valtio/utils'; import { devtools, subscribeKey } from 'valtio/utils';
import { XrplClient } from "xrpl-client"; import { XrplClient } from "xrpl-client";
import { SplitSize } from "./actions/persistSplits"; import { SplitSize } from "./actions/persistSplits";
@@ -13,9 +13,11 @@ export interface IFile {
name: string; name: string;
language: string; language: string;
content: string; content: string;
compiledValueSnapshot?: string
compiledContent?: ArrayBuffer | null; compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null; compiledWatContent?: string | null;
lastCompiled?: Date lastCompiled?: Date
containsErrors?: boolean
} }
export interface FaucetAccountRes { export interface FaucetAccountRes {
@@ -52,6 +54,8 @@ export interface ILog {
defaultCollapsed?: boolean defaultCollapsed?: boolean
} }
export type DeployValue = Record<IFile['name'], any>;
export interface IState { export interface IState {
files: IFile[]; files: IFile[];
gistId?: string | null; gistId?: string | null;
@@ -82,7 +86,8 @@ export interface IState {
compileOptions: { compileOptions: {
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os'; optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
strip: boolean strip: boolean
} },
deployValues: DeployValue
} }
// let localStorageState: null | string = null; // let localStorageState: null | string = null;
@@ -116,7 +121,8 @@ let initialState: IState = {
compileOptions: { compileOptions: {
optimizationLevel: '-O2', optimizationLevel: '-O2',
strip: true strip: true
} },
deployValues: {}
}; };
let localStorageAccounts: string | null = null; let localStorageAccounts: string | null = null;
@@ -162,16 +168,23 @@ if (process.env.NODE_ENV !== "production") {
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
subscribe(state, () => { subscribe(state.accounts, () => {
const { accounts, active } = state; const { accounts } = state;
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false })) const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading)); localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
if (!state.files[active]?.compiledWatContent) {
state.activeWat = 0;
} else {
state.activeWat = active;
}
}); });
const updateActiveWat = () => {
const filename = state.files[state.active]?.name
const compiledFiles = state.files.filter(
file => file.compiledContent)
const idx = compiledFiles.findIndex(file => file.name === filename)
if (idx !== -1) state.activeWat = idx
}
subscribeKey(state, 'active', updateActiveWat)
subscribeKey(state, 'files', updateActiveWat)
} }
export default state export default state

View File

@@ -18,14 +18,13 @@ export interface TransactionState {
txIsDisabled: boolean; txIsDisabled: boolean;
txFields: TxFields; txFields: TxFields;
viewType: 'json' | 'ui', viewType: 'json' | 'ui',
editorSavedValue: null | string,
editorValue?: string, editorValue?: string,
estimatedFee?: string estimatedFee?: string
} }
export type TxFields = Omit< export type TxFields = Omit<
typeof transactionsData[0], Partial<typeof transactionsData[0]>,
"Account" | "Sequence" | "TransactionType" "Account" | "Sequence" | "TransactionType"
>; >;
@@ -36,15 +35,14 @@ export const defaultTransaction: TransactionState = {
txIsLoading: false, txIsLoading: false,
txIsDisabled: false, txIsDisabled: false,
txFields: {}, txFields: {},
viewType: 'ui', viewType: 'ui'
editorSavedValue: null
}; };
export const transactionsState = proxy({ export const transactionsState = proxy({
transactions: [ transactions: [
{ {
header: "test1.json", header: "test1.json",
state: defaultTransaction, state: { ...defaultTransaction },
}, },
], ],
activeHeader: "test1.json" activeHeader: "test1.json"
@@ -92,7 +90,7 @@ export const modifyTransaction = (
} }
Object.keys(partialTx).forEach(k => { Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definetly safe! // Typescript mess here, but is definitely safe!
const s = tx.state as any; const s = tx.state as any;
const p = partialTx as any; // ? Make copy const p = partialTx as any; // ? Make copy
if (!deepEqual(s[k], p[k])) s[k] = p[k]; if (!deepEqual(s[k], p[k])) s[k] = p[k];
@@ -118,7 +116,7 @@ export const prepareTransaction = (data: any) => {
// handle type: `json` // handle type: `json`
if (_value && typeof _value === "object" && _value.$type === "json") { if (_value && typeof _value === "object" && _value.$type === "json") {
if (typeof _value.$value === "object") { if (typeof _value.$value === "object") {
options[field] = _value.$value as any; options[field] = _value.$value;
} else { } else {
try { try {
options[field] = JSON.parse(_value.$value); options[field] = JSON.parse(_value.$value);
@@ -131,8 +129,8 @@ export const prepareTransaction = (data: any) => {
} }
} }
// delete unneccesary fields // delete unnecessary fields
if (options[field] === undefined) { if (!options[field]) {
delete options[field]; delete options[field];
} }
}); });
@@ -152,7 +150,7 @@ export const prepareState = (value: string, transactionType?: string) => {
const { Account, TransactionType, Destination, ...rest } = options; const { Account, TransactionType, Destination, ...rest } = options;
let tx: Partial<TransactionState> = {}; let tx: Partial<TransactionState> = {};
const txFields = getTxFields(transactionType) const schema = getTxFields(transactionType)
if (Account) { if (Account) {
const acc = state.accounts.find(acc => acc.address === Account); const acc = state.accounts.find(acc => acc.address === Account);
@@ -180,9 +178,8 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedTransaction = null; tx.selectedTransaction = null;
} }
if (txFields.Destination !== undefined) { if (schema.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination); const dest = state.accounts.find(acc => acc.address === Destination);
rest.Destination = null
if (dest) { if (dest) {
tx.selectedDestAccount = { tx.selectedDestAccount = {
label: dest.name, label: dest.name,
@@ -199,11 +196,14 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedDestAccount = null tx.selectedDestAccount = null
} }
} }
else if (Destination) {
rest.Destination = Destination
}
Object.keys(rest).forEach(field => { Object.keys(rest).forEach(field => {
const value = rest[field]; const value = rest[field];
const origValue = txFields[field as keyof TxFields] const schemaVal = schema[field as keyof TxFields]
const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.$type === 'xrp' const isXrp = typeof value !== 'object' && schemaVal && typeof schemaVal === 'object' && schemaVal.$type === 'xrp'
if (isXrp) { if (isXrp) {
rest[field] = { rest[field] = {
$type: "xrp", $type: "xrp",
@@ -218,7 +218,6 @@ export const prepareState = (value: string, transactionType?: string) => {
}); });
tx.txFields = rest; tx.txFields = rest;
tx.editorSavedValue = null;
return tx return tx
} }
@@ -244,3 +243,10 @@ export const getTxFields = (tt?: string) => {
} }
export { transactionsData } export { transactionsData }
export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment')

View File

@@ -53,6 +53,7 @@ export const {
accent: "#9D2DFF", accent: "#9D2DFF",
background: "$gray1", background: "$gray1",
backgroundAlt: "$gray4", backgroundAlt: "$gray4",
backgroundOverlay: "$mauve2",
text: "$gray12", text: "$gray12",
textMuted: "$gray10", textMuted: "$gray10",
primary: "$plum", primary: "$plum",
@@ -365,6 +366,7 @@ export const darkTheme = createTheme("dark", {
...greenDark, ...greenDark,
...redDark, ...redDark,
deep: "rgb(10, 10, 10)", deep: "rgb(10, 10, 10)",
backgroundOverlay: "$mauve5"
// backgroundA: transparentize(0.1, grayDark.gray1), // backgroundA: transparentize(0.1, grayDark.gray1),
}, },
}); });

21
styles/keyframes.ts Normal file
View File

@@ -0,0 +1,21 @@
import { keyframes } from '../stitches.config';
export const slideUpAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
export const slideRightAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(-2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
export const slideDownAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(-2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
export const slideLeftAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});

24
utils/comment-parser.ts Normal file
View File

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

View File

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

View File

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

133
yarn.lock
View File

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