Compare commits

..

3 Commits

Author SHA1 Message Date
JaniAnttonen
8430c9b553 Use explicit types 2022-04-05 15:04:24 +03:00
JaniAnttonen
1c66c9e572 Let logs through even when there are no accounts 2022-04-05 12:45:20 +03:00
JaniAnttonen
2b518f80e5 Listen to connection disposals 2022-04-05 12:44:14 +03:00
135 changed files with 9390 additions and 14194 deletions

View File

@@ -1,12 +1,9 @@
NEXTAUTH_URL=https://example.com
NEXTAUTH_SECRET="1234"
GITHUB_SECRET=""
GITHUB_ID=""
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000"
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v3.xrpl-labs.com"
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v3-debugstream.xrpl-labs.com"
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v3-explorer.xrpl-labs.com"
NEXT_PUBLIC_NETWORK_ID="21338"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v2.xrpl-labs.com"
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v2-debugstream.xrpl-labs.com"
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v2-explorer.xrpl-labs.com"
NEXT_PUBLIC_SITE_URL=http://localhost:3000

5
.gitignore vendored
View File

@@ -32,8 +32,3 @@ yarn-error.log*
# vercel
.vercel
.vscode
# yarn
.yarnrc.yml
.yarn/

View File

@@ -1,38 +1 @@
See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
.vscode
*.md
utils/libwabt.js
*.md

View File

@@ -1,8 +0,0 @@
{
"tabWidth": 2,
"arrowParens": "avoid",
"semi": false,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "none"
}

View File

@@ -1,8 +1,6 @@
# XRPL Hooks Builder
# XRPL Hooks IDE
https://hooks-builder.xrpl.org/
This is the repository for XRPL Hooks Builder. This project is built with Next.JS
This is the repository for XRPL Hooks IDE. This project is built with Next.JS
## General
@@ -108,5 +106,3 @@ To learn more about Next.js, take a look at the following resources:
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

@@ -1,94 +1,93 @@
import toast from 'react-hot-toast'
import { useSnapshot } from 'valtio'
import { ArrowSquareOut, Copy, Trash, Wallet, X } from 'phosphor-react'
import React, { useEffect, useState, FC } from 'react'
import Dinero from 'dinero.js'
import toast from "react-hot-toast";
import { useSnapshot } from "valtio";
import { ArrowSquareOut, Copy, Trash, Wallet, X } from "phosphor-react";
import React, { useEffect, useState, FC } from "react";
import Dinero from "dinero.js";
import Button from './Button'
import { addFaucetAccount, importAccount } from '../state/actions'
import state from '../state'
import Box from './Box'
import { Container, Heading, Stack, Text, Flex } from '.'
import Button from "./Button";
import { addFaucetAccount, importAccount } from "../state/actions";
import state from "../state";
import Box from "./Box";
import { Container, Heading, Stack, Text, Flex } from ".";
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
DialogTrigger
} from './Dialog'
import { css } from '../stitches.config'
import { Input, Label } from './Input'
import truncate from '../utils/truncate'
DialogTrigger,
} from "./Dialog";
import { css } from "../stitches.config";
import { Input } from "./Input";
import truncate from "../utils/truncate";
const labelStyle = css({
color: '$mauve10',
textTransform: 'uppercase',
fontSize: '10px',
mb: '$0.5'
})
import transactionsData from '../content/transactions.json'
import { SetHookDialog } from './SetHookDialog'
import { addFunds } from '../state/actions/addFaucetAccount'
import { deleteHook } from '../state/actions/deployHook'
import { capitalize } from '../utils/helpers'
import { deleteAccount } from '../state/actions/deleteAccount'
import { xrplSend } from '../state/actions/xrpl-client'
color: "$mauve10",
textTransform: "uppercase",
fontSize: "10px",
mb: "$0.5",
});
import transactionsData from "../content/transactions.json";
import { SetHookDialog } from "./SetHookDialog";
import { addFunds } from "../state/actions/addFaucetAccount";
import { deleteHook } from "../state/actions/deployHook";
export const AccountDialog = ({
activeAccountAddress,
setActiveAccountAddress
setActiveAccountAddress,
}: {
activeAccountAddress: string | null
setActiveAccountAddress: React.Dispatch<React.SetStateAction<string | null>>
activeAccountAddress: string | null;
setActiveAccountAddress: React.Dispatch<React.SetStateAction<string | null>>;
}) => {
const snap = useSnapshot(state)
const [showSecret, setShowSecret] = useState(false)
const activeAccount = snap.accounts.find(account => account.address === activeAccountAddress)
const snap = useSnapshot(state);
const [showSecret, setShowSecret] = useState(false);
const activeAccount = snap.accounts.find(
(account) => account.address === activeAccountAddress
);
return (
<Dialog
open={Boolean(activeAccountAddress)}
onOpenChange={open => {
setShowSecret(false)
!open && setActiveAccountAddress(null)
onOpenChange={(open) => {
setShowSecret(false);
!open && setActiveAccountAddress(null);
}}
>
<DialogContent
css={{
backgroundColor: '$mauve1 !important',
border: '1px solid $mauve2',
'.dark &': {
backgroundColor: "$mauve1 !important",
border: "1px solid $mauve2",
".dark &": {
// backgroundColor: "$black !important",
},
p: '$3',
'&:before': {
content: ' ',
position: 'absolute',
p: "$3",
"&:before": {
content: " ",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
opacity: 0.2,
'.dark &': {
opacity: 1
".dark &": {
opacity: 1,
},
zIndex: 0,
pointerEvents: 'none',
pointerEvents: "none",
backgroundImage: `url('/pattern-dark.svg'), url('/pattern-dark-2.svg')`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'bottom left, top right'
}
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left, top right",
},
}}
>
<DialogTitle
css={{
display: 'flex',
width: '100%',
alignItems: 'center',
borderBottom: '1px solid $mauve6',
pb: '$3',
gap: '$3',
fontSize: '$md'
display: "flex",
width: "100%",
alignItems: "center",
borderBottom: "1px solid $mauve6",
pb: "$3",
gap: "$3",
fontSize: "$md",
}}
>
<Wallet size="15px" /> {activeAccount?.name}
@@ -96,180 +95,164 @@ export const AccountDialog = ({
<Button
size="xs"
outline
css={{ ml: 'auto', mr: '$9' }}
css={{ ml: "auto", mr: "$9" }}
tabIndex={-1}
onClick={() => {
deleteAccount(activeAccount?.address)
const index = state.accounts.findIndex(
(acc) => acc.address === activeAccount?.address
);
state.accounts.splice(index, 1);
}}
>
Delete Account <Trash size="15px" />
</Button>
</DialogClose>
</DialogTitle>
<DialogDescription as="div" css={{ fontFamily: '$monospace' }}>
<Stack css={{ display: 'flex', flexDirection: 'column', gap: '$3' }}>
<Flex css={{ alignItems: 'center' }}>
<Flex css={{ flexDirection: 'column' }}>
<DialogDescription as="div" css={{ fontFamily: "$monospace" }}>
<Stack css={{ display: "flex", flexDirection: "column", gap: "$3" }}>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Account Address</Text>
<Text
css={{
fontFamily: '$monospace',
a: { '&:hover': { textDecoration: 'underline' } }
fontFamily: "$monospace",
}}
>
<a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noopener noreferrer"
>
{activeAccount?.address}
</a>
{activeAccount?.address}
</Text>
</Flex>
<Flex css={{ marginLeft: 'auto', color: '$mauve12' }}>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
<Button
size="sm"
ghost
css={{ mt: '$3' }}
css={{ mt: "$3" }}
onClick={() => {
navigator.clipboard.writeText(activeAccount?.address || '')
toast.success('Copied address to clipboard')
navigator.clipboard.writeText(activeAccount?.address || "");
toast.success("Copied address to clipboard");
}}
>
<Copy size="15px" />
</Button>
</Flex>
</Flex>
<Flex css={{ alignItems: 'center' }}>
<Flex css={{ flexDirection: 'column' }}>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Secret</Text>
<Text
as="div"
css={{
fontFamily: '$monospace',
display: 'flex',
alignItems: 'center'
fontFamily: "$monospace",
display: "flex",
alignItems: "center",
}}
>
{showSecret
? activeAccount?.secret
: '•'.repeat(activeAccount?.secret.length || 16)}{' '}
: "•".repeat(activeAccount?.secret.length || 16)}{" "}
<Button
css={{
fontFamily: '$monospace',
fontFamily: "$monospace",
lineHeight: 2,
mt: '2px',
ml: '$3'
mt: "2px",
ml: "$3",
}}
ghost
size="xs"
onClick={() => setShowSecret(curr => !curr)}
onClick={() => setShowSecret((curr) => !curr)}
>
{showSecret ? 'Hide' : 'Show'}
{showSecret ? "Hide" : "Show"}
</Button>
</Text>
</Flex>
<Flex css={{ marginLeft: 'auto', color: '$mauve12' }}>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
<Button
size="sm"
ghost
onClick={() => {
navigator.clipboard.writeText(activeAccount?.secret || '')
toast.success('Copied secret to clipboard')
navigator.clipboard.writeText(activeAccount?.secret || "");
toast.success("Copied secret to clipboard");
}}
css={{ mt: '$3' }}
css={{ mt: "$3" }}
>
<Copy size="15px" />
</Button>
</Flex>
</Flex>
<Flex css={{ alignItems: 'center' }}>
<Flex css={{ flexDirection: 'column' }}>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Balances & Objects</Text>
<Text
css={{
fontFamily: '$monospace',
display: 'flex',
alignItems: 'center'
fontFamily: "$monospace",
display: "flex",
alignItems: "center",
}}
>
{Dinero({
amount: Number(activeAccount?.xrp || '0'),
precision: 6
amount: Number(activeAccount?.xrp || "0"),
precision: 6,
})
.toUnit()
.toLocaleString(undefined, {
style: 'currency',
currency: 'XRP',
currencyDisplay: 'name'
style: "currency",
currency: "XRP",
currencyDisplay: "name",
})}
<Button
css={{
fontFamily: '$monospace',
fontFamily: "$monospace",
lineHeight: 2,
mt: '2px',
ml: '$3'
mt: "2px",
ml: "$3",
}}
ghost
size="xs"
onClick={() => {
addFunds(activeAccount?.address || '')
addFunds(activeAccount?.address || "");
}}
>
Add Funds
</Button>
</Text>
</Flex>
<Flex
css={{
marginLeft: 'auto'
}}
>
<Flex css={{ marginLeft: "auto" }}>
<a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noreferrer noopener"
>
<Button size="sm" ghost css={{ color: '$grass11 !important', mt: '$3' }}>
<Button
size="sm"
ghost
css={{ color: "$grass11 !important", mt: "$3" }}
>
<ArrowSquareOut size="15px" />
</Button>
</a>
</Flex>
</Flex>
<Flex css={{ alignItems: 'center' }}>
<Flex css={{ flexDirection: 'column' }}>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Installed Hooks</Text>
<Text
css={{
fontFamily: '$monospace',
a: { '&:hover': { textDecoration: 'underline' } }
fontFamily: "$monospace",
}}
>
{activeAccount && activeAccount.hooks.length > 0
? 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>
)
})
: ''}
? activeAccount.hooks.map((i) => truncate(i, 12)).join(",")
: ""}
</Text>
</Flex>
{activeAccount && activeAccount?.hooks?.length > 0 && (
<Flex css={{ marginLeft: 'auto' }}>
<Flex css={{ marginLeft: "auto" }}>
<Button
size="xs"
outline
disabled={activeAccount.isLoading}
css={{ mt: '$3', mr: '$1', ml: 'auto' }}
css={{ mt: "$3", mr: "$1", ml: "auto" }}
onClick={() => {
deleteHook(activeAccount)
deleteHook(activeAccount);
}}
>
Delete Hook <Trash size="15px" />
@@ -280,109 +263,105 @@ export const AccountDialog = ({
</Stack>
</DialogDescription>
<DialogClose asChild>
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)
}
);
};
interface AccountProps {
card?: boolean
hideDeployBtn?: boolean
showHookStats?: boolean
card?: boolean;
hideDeployBtn?: boolean;
showHookStats?: boolean;
}
const Accounts: FC<AccountProps> = props => {
const snap = useSnapshot(state)
const [activeAccountAddress, setActiveAccountAddress] = useState<string | null>(null)
const Accounts: FC<AccountProps> = (props) => {
const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState<
string | null
>(null);
useEffect(() => {
const fetchAccInfo = async () => {
if (snap.clientStatus === 'online') {
const requests = snap.accounts.map(acc =>
xrplSend({
if (snap.clientStatus === "online") {
const requests = snap.accounts.map((acc) =>
snap.client?.send({
id: `hooks-builder-req-info-${acc.address}`,
command: 'account_info',
account: acc.address
command: "account_info",
account: acc.address,
})
)
const responses = await Promise.all(requests)
);
const responses = await Promise.all(requests);
responses.forEach((res: any) => {
const address = res?.account_data?.Account as string
const balance = res?.account_data?.Balance as string
const sequence = res?.account_data?.Sequence as number
const accountToUpdate = state.accounts.find(acc => acc.address === address)
const address = res?.account_data?.Account as string;
const balance = res?.account_data?.Balance as string;
const sequence = res?.account_data?.Sequence as number;
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.xrp = balance
accountToUpdate.sequence = sequence
accountToUpdate.error = null
} else {
const oldAccount = state.accounts.find(acc => acc.address === res?.account)
if (oldAccount) {
oldAccount.xrp = '0'
oldAccount.error = {
code: res?.error,
message: res?.error_message
}
}
accountToUpdate.xrp = balance;
accountToUpdate.sequence = sequence;
}
})
const objectRequests = snap.accounts.map(acc => {
return xrplSend({
});
const objectRequests = snap.accounts.map((acc) => {
return snap.client?.send({
id: `hooks-builder-req-objects-${acc.address}`,
command: 'account_objects',
account: acc.address
})
})
const objectResponses = await Promise.all(objectRequests)
command: "account_objects",
account: acc.address,
});
});
const objectResponses = await Promise.all(objectRequests);
objectResponses.forEach((res: any) => {
const address = res?.account as string
const accountToUpdate = state.accounts.find(acc => acc.address === address)
const address = res?.account as string;
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.hooks =
res.account_objects
.find((ac: any) => ac?.LedgerEntryType === 'Hook')
?.Hooks?.map((oo: any) => oo.Hook.HookHash) || []
.find((ac: any) => ac?.LedgerEntryType === "Hook")
?.Hooks?.map((oo: any) => oo.Hook.HookHash) || [];
}
})
});
}
}
};
let fetchAccountInfoInterval: NodeJS.Timer
if (snap.clientStatus === 'online') {
fetchAccInfo()
fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 10000)
let fetchAccountInfoInterval: NodeJS.Timer;
if (snap.clientStatus === "online") {
fetchAccInfo();
fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 10000);
}
return () => {
if (snap.accounts.length > 0) {
if (fetchAccountInfoInterval) {
clearInterval(fetchAccountInfoInterval)
clearInterval(fetchAccountInfoInterval);
}
}
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [snap.accounts.length, snap.clientStatus])
}, [snap.accounts, snap.clientStatus]);
return (
<Box
as="div"
css={{
display: 'flex',
backgroundColor: props.card ? '$deep' : '$mauve1',
position: 'relative',
flex: '1',
height: '100%',
border: '1px solid $mauve6',
borderRadius: props.card ? '$md' : undefined
display: "flex",
backgroundColor: props.card ? "$deep" : "$mauve1",
position: "relative",
flex: "1",
height: "100%",
border: "1px solid $mauve6",
borderRadius: props.card ? "$md" : undefined,
}}
>
<Container css={{ p: 0, flexShrink: 1, height: '100%' }}>
<Container css={{ p: 0, flexShrink: 1, height: "100%" }}>
<Flex
css={{
py: '$3',
borderBottom: props.card ? '1px solid $mauve6' : undefined
py: "$3",
borderBottom: props.card ? "1px solid $mauve6" : undefined,
}}
>
<Heading
@@ -390,100 +369,97 @@ const Accounts: FC<AccountProps> = props => {
css={{
fontWeight: 300,
m: 0,
fontSize: '11px',
color: '$mauve12',
px: '$3',
textTransform: 'uppercase',
alignItems: 'center',
display: 'inline-flex',
gap: '$3'
fontSize: "11px",
color: "$mauve12",
px: "$3",
textTransform: "uppercase",
alignItems: "center",
display: "inline-flex",
gap: "$3",
}}
>
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
</Heading>
<Flex css={{ ml: 'auto', gap: '$3', marginRight: '$3' }}>
<ImportAccountDialog type="create" />
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
<Button ghost size="sm" onClick={() => addFaucetAccount(true)}>
Create
</Button>
<ImportAccountDialog />
</Flex>
</Flex>
<Stack
css={{
flexDirection: 'column',
width: '100%',
fontSize: '13px',
wordWrap: 'break-word',
fontWeight: '$body',
flexDirection: "column",
width: "100%",
fontSize: "13px",
wordWrap: "break-word",
fontWeight: "$body",
gap: 0,
height: 'calc(100% - 52px)',
flexWrap: 'nowrap',
overflowY: 'auto'
height: "calc(100% - 52px)",
flexWrap: "nowrap",
overflowY: "auto",
}}
>
{snap.accounts.map(account => (
{snap.accounts.map((account) => (
<Flex
column
key={account.address + account.name}
onClick={() => setActiveAccountAddress(account.address)}
css={{
px: '$3',
py: props.card ? '$3' : '$2',
cursor: 'pointer',
borderBottom: props.card ? '1px solid $mauve6' : undefined,
'@hover': {
'&:hover': {
background: '$backgroundAlt'
}
}
px: "$3",
py: props.card ? "$3" : "$2",
cursor: "pointer",
borderBottom: props.card ? "1px solid $mauve6" : undefined,
"@hover": {
"&:hover": {
background: "$backgroundAlt",
},
},
}}
>
<Flex
row
css={{
justifyContent: 'space-between'
justifyContent: "space-between",
}}
>
<Box>
<Text>{account.name} </Text>
<Text
css={{
color: '$textMuted',
wordBreak: 'break-word'
color: "$textMuted",
wordBreak: "break-word",
}}
>
{account.address}{' '}
{!account?.error ? (
`(${Dinero({
amount: Number(account?.xrp || '0'),
precision: 6
})
.toUnit()
.toLocaleString(undefined, {
style: 'currency',
currency: 'XRP',
currencyDisplay: 'name'
})})`
) : (
<Box css={{ color: '$red11' }}>
(Account not found, request funds to activate account)
</Box>
)}
{account.address} (
{Dinero({
amount: Number(account?.xrp || "0"),
precision: 6,
})
.toUnit()
.toLocaleString(undefined, {
style: "currency",
currency: "XRP",
currencyDisplay: "name",
})}
)
</Text>
</Box>
{!props.hideDeployBtn && (
<div
className="hook-deploy-button"
onClick={e => {
e.stopPropagation()
onClick={(e) => {
e.stopPropagation();
}}
>
<SetHookDialog accountAddress={account.address} />
<SetHookDialog account={account} />
</div>
)}
</Flex>
{props.showHookStats && (
<Text muted small css={{ mt: '$2' }}>
<Text muted small css={{ mt: "$2" }}>
{account.hooks.length} hook
{account.hooks.length === 1 ? '' : 's'} installed
{account.hooks.length === 1 ? "" : "s"} installed
</Text>
)}
</Flex>
@@ -495,95 +471,65 @@ const Accounts: FC<AccountProps> = props => {
setActiveAccountAddress={setActiveAccountAddress}
/>
</Box>
)
}
);
};
export const transactionsOptions = transactionsData.map(tx => ({
export const transactionsOptions = transactionsData.map((tx) => ({
value: tx.TransactionType,
label: tx.TransactionType
}))
label: tx.TransactionType,
}));
const ImportAccountDialog = ({ 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('')
}
const ImportAccountDialog = () => {
const [value, setValue] = useState("");
return (
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm">
{btnText}
Import
</Button>
</DialogTrigger>
<DialogContent aria-describedby={undefined}>
<DialogTitle css={{ mb: '$4' }}>{title}</DialogTitle>
<Flex column>
<Box css={{ mb: '$2' }}>
<Label>
Account name <Text muted>(optional)</Text>
</Label>
<Input
name="name"
type="text"
autoComplete="off"
autoCapitalize="on"
value={name}
onChange={e => setName(e.target.value)}
/>
</Box>
{type === 'import' && (
<Box>
<Label>Account secret</Label>
<Input
required
name="secret"
type="password"
autoComplete="new-password"
value={secret}
onChange={e => setSecret(e.target.value)}
/>
</Box>
)}
</Flex>
<DialogContent>
<DialogTitle>Import account</DialogTitle>
<DialogDescription>
<label>Add account secret</label>
<Input
name="secret"
type="password"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: 'flex-end',
gap: '$3'
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button type="submit" variant="primary" onClick={handleSubmit}>
{title}
<Button
variant="primary"
onClick={() => {
importAccount(value);
setValue("");
}}
>
Import account
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)
}
);
};
export default Accounts
export default Accounts;

View File

@@ -0,0 +1,88 @@
import React from "react";
import { blackA } from "@radix-ui/colors";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { styled, keyframes } from "../stitches.config";
const overlayShow = keyframes({
"0%": { opacity: 0 },
"100%": { opacity: 1 },
});
const contentShow = keyframes({
"0%": { opacity: 0, transform: "translate(-50%, -48%) scale(.96)" },
"100%": { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
});
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
zIndex: 1000,
backgroundColor: blackA.blackA9,
position: "fixed",
inset: 0,
"@media (prefers-reduced-motion: no-preference)": {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
".dark &": {
backgroundColor: blackA.blackA11,
},
});
const Root: React.FC<AlertDialogPrimitive.AlertDialogProps> = ({
children,
...rest
}) => {
return (
<AlertDialogPrimitive.Root {...rest}>
<StyledOverlay />
{children}
</AlertDialogPrimitive.Root>
);
};
const StyledContent = styled(AlertDialogPrimitive.Content, {
zIndex: 1000,
backgroundColor: "$mauve2",
color: "$mauve12",
borderRadius: "$md",
boxShadow:
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "90vw",
maxWidth: "450px",
maxHeight: "85vh",
padding: 25,
"@media (prefers-reduced-motion: no-preference)": {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
"&:focus": { outline: "none" },
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
"0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
},
});
const StyledTitle = styled(AlertDialogPrimitive.Title, {
margin: 0,
color: "$mauve12",
fontWeight: 500,
fontSize: "$lg",
});
const StyledDescription = styled(AlertDialogPrimitive.Description, {
marginBottom: 20,
color: "$mauve11",
lineHeight: 1.5,
fontSize: "$sm",
});
// Exports
export const AlertDialog = Root;
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
export const AlertDialogContent = StyledContent;
export const AlertDialogTitle = StyledTitle;
export const AlertDialogDescription = StyledDescription;
export const AlertDialogAction = AlertDialogPrimitive.Action;
export const AlertDialogCancel = AlertDialogPrimitive.Cancel;

View File

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

View File

@@ -1,83 +0,0 @@
import React from 'react'
import { blackA } from '@radix-ui/colors'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import { styled, keyframes } from '../../stitches.config'
const overlayShow = keyframes({
'0%': { opacity: 0 },
'100%': { opacity: 1 }
})
const contentShow = keyframes({
'0%': { opacity: 0, transform: 'translate(-50%, -48%) scale(.96)' },
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' }
})
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
zIndex: 1000,
backgroundColor: blackA.blackA9,
position: 'fixed',
inset: 0,
'@media (prefers-reduced-motion: no-preference)': {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
},
'.dark &': {
backgroundColor: blackA.blackA11
}
})
const Root: React.FC<AlertDialogPrimitive.AlertDialogProps> = ({ children, ...rest }) => {
return (
<AlertDialogPrimitive.Root {...rest}>
<StyledOverlay />
{children}
</AlertDialogPrimitive.Root>
)
}
const StyledContent = styled(AlertDialogPrimitive.Content, {
zIndex: 1000,
backgroundColor: '$mauve2',
color: '$mauve12',
borderRadius: '$md',
boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '90vw',
maxWidth: '450px',
maxHeight: '85vh',
padding: 25,
'@media (prefers-reduced-motion: no-preference)': {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
},
'&:focus': { outline: 'none' },
'.dark &': {
backgroundColor: '$mauve5',
boxShadow: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)'
}
})
const StyledTitle = styled(AlertDialogPrimitive.Title, {
margin: 0,
color: '$mauve12',
fontWeight: 500,
fontSize: '$lg'
})
const StyledDescription = styled(AlertDialogPrimitive.Description, {
marginBottom: 20,
color: '$mauve11',
lineHeight: 1.5,
fontSize: '$md'
})
// Exports
export const AlertDialog = Root
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
export const AlertDialogContent = StyledContent
export const AlertDialogTitle = StyledTitle
export const AlertDialogDescription = StyledDescription
export const AlertDialogAction = AlertDialogPrimitive.Action
export const AlertDialogCancel = AlertDialogPrimitive.Cancel

View File

@@ -1,8 +1,8 @@
import { styled } from '../stitches.config'
import { styled } from "../stitches.config";
const Box = styled('div', {
const Box = styled("div", {
// all: "unset",
boxSizing: 'border-box'
})
boxSizing: "border-box",
});
export default Box
export default Box;

View File

@@ -1,285 +1,291 @@
import React from 'react'
import { styled } from '../stitches.config'
import Flex from './Flex'
import Spinner from './Spinner'
import React from "react";
import { styled } from "../stitches.config";
import Flex from "./Flex";
import Spinner from "./Spinner";
export const StyledButton = styled('button', {
export const StyledButton = styled("button", {
// Reset
all: 'unset',
position: 'relative',
appereance: 'none',
fontFamily: '$body',
alignItems: 'center',
boxSizing: 'border-box',
userSelect: 'none',
'&::before': {
boxSizing: 'border-box'
all: "unset",
position: "relative",
appereance: "none",
fontFamily: "$body",
alignItems: "center",
boxSizing: "border-box",
userSelect: "none",
"&::before": {
boxSizing: "border-box",
},
'&::after': {
boxSizing: 'border-box'
"&::after": {
boxSizing: "border-box",
},
// Custom reset?
display: 'inline-flex',
display: "inline-flex",
flexShrink: 0,
justifyContent: 'center',
lineHeight: '1',
gap: '5px',
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
justifyContent: "center",
lineHeight: "1",
gap: "5px",
WebkitTapHighlightColor: "rgba(0,0,0,0)",
// Custom
height: '$6',
px: '$2',
fontSize: '$2',
height: "$6",
px: "$2",
fontSize: "$2",
fontWeight: 500,
fontVariantNumeric: 'tabular-nums',
cursor: 'pointer',
width: 'max-content',
'&:disabled': {
fontVariantNumeric: "tabular-nums",
cursor: "pointer",
width: "max-content",
"&:disabled": {
opacity: 0.6,
pointerEvents: 'none',
cursor: 'not-allowed'
pointerEvents: "none",
cursor: "not-allowed",
},
variants: {
size: {
xs: {
borderRadius: '$sm',
height: '$5',
px: '$2',
fontSize: '$xs'
borderRadius: "$sm",
height: "$5",
px: "$2",
fontSize: "$xs",
},
sm: {
borderRadius: '$sm',
height: '$7',
px: '$3',
fontSize: '$xs'
borderRadius: "$sm",
height: "$7",
px: "$3",
fontSize: "$xs",
},
md: {
borderRadius: '$sm',
height: '$8',
px: '$3',
fontSize: '$xs'
borderRadius: "$sm",
height: "$8",
px: "$3",
fontSize: "$xs",
},
lg: {
borderRadius: '$sm',
height: '$10',
px: '$4',
fontSize: '$xs'
}
borderRadius: "$sm",
height: "$10",
px: "$4",
fontSize: "$xs",
},
},
variant: {
link: {
textDecoration: 'underline',
fontSize: 'inherit',
color: '$textMuted',
textUnderlineOffset: '2px'
textDecoration: "underline",
fontSize: "inherit",
color: "$textMuted",
textUnderlineOffset: "2px",
},
default: {
backgroundColor: '$mauve12',
boxShadow: 'inset 0 0 0 1px $colors$mauve12',
color: '$mauve1',
'@hover': {
'&:hover': {
backgroundColor: '$mauve12',
boxShadow: 'inset 0 0 0 1px $colors$mauve12'
}
backgroundColor: "$mauve12",
boxShadow: "inset 0 0 0 1px $colors$mauve12",
color: "$mauve1",
"@hover": {
"&:hover": {
backgroundColor: "$mauve12",
boxShadow: "inset 0 0 0 1px $colors$mauve12",
},
},
'&:active': {
backgroundColor: '$mauve10',
boxShadow: 'inset 0 0 0 1px $colors$mauve11'
"&:active": {
backgroundColor: "$mauve10",
boxShadow: "inset 0 0 0 1px $colors$mauve11",
},
'&:focus': {
boxShadow: 'inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12'
"&:focus": {
boxShadow:
"inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
backgroundColor: '$mauve4',
boxShadow: 'inset 0 0 0 1px $colors$mauve8'
}
backgroundColor: "$mauve4",
boxShadow: "inset 0 0 0 1px $colors$mauve8",
},
},
primary: {
backgroundColor: `$accent`,
boxShadow: 'inset 0 0 0 1px $colors$purple9',
color: '$white',
'@hover': {
'&:hover': {
backgroundColor: '$purple10',
boxShadow: 'inset 0 0 0 1px $colors$purple11'
}
boxShadow: "inset 0 0 0 1px $colors$purple9",
color: "$white",
"@hover": {
"&:hover": {
backgroundColor: "$purple10",
boxShadow: "inset 0 0 0 1px $colors$purple11",
},
},
'&:active': {
backgroundColor: '$purple8',
boxShadow: 'inset 0 0 0 1px $colors$purple8'
"&:active": {
backgroundColor: "$purple8",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
'&:focus': {
boxShadow: 'inset 0 0 0 2px $colors$purple12'
"&:focus": {
boxShadow: "inset 0 0 0 2px $colors$purple12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
backgroundColor: '$mauve4',
boxShadow: 'inset 0 0 0 1px $colors$purple8'
}
backgroundColor: "$mauve4",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
},
secondary: {
backgroundColor: `$purple9`,
boxShadow: 'inset 0 0 0 1px $colors$purple9',
color: '$white',
'@hover': {
'&:hover': {
backgroundColor: '$purple10',
boxShadow: 'inset 0 0 0 1px $colors$purple11'
}
boxShadow: "inset 0 0 0 1px $colors$purple9",
color: "$white",
"@hover": {
"&:hover": {
backgroundColor: "$purple10",
boxShadow: "inset 0 0 0 1px $colors$purple11",
},
},
'&:active': {
backgroundColor: '$purple8',
boxShadow: 'inset 0 0 0 1px $colors$purple8'
"&:active": {
backgroundColor: "$purple8",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
'&:focus': {
boxShadow: 'inset 0 0 0 2px $colors$purple12'
"&:focus": {
boxShadow: "inset 0 0 0 2px $colors$purple12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
backgroundColor: '$mauve4',
boxShadow: 'inset 0 0 0 1px $colors$purple8'
}
backgroundColor: "$mauve4",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
},
destroy: {
backgroundColor: `$red9`,
boxShadow: 'inset 0 0 0 1px $colors$red9',
color: '$white',
'@hover': {
'&:hover': {
backgroundColor: '$red10',
boxShadow: 'inset 0 0 0 1px $colors$red11'
}
boxShadow: "inset 0 0 0 1px $colors$red9",
color: "$white",
"@hover": {
"&:hover": {
backgroundColor: "$red10",
boxShadow: "inset 0 0 0 1px $colors$red11",
},
},
'&:active': {
backgroundColor: '$red8',
boxShadow: 'inset 0 0 0 1px $colors$red8'
"&:active": {
backgroundColor: "$red8",
boxShadow: "inset 0 0 0 1px $colors$red8",
},
'&:focus': {
boxShadow: 'inset 0 0 0 2px $colors$red12'
"&:focus": {
boxShadow: "inset 0 0 0 2px $colors$red12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
backgroundColor: '$mauve4',
boxShadow: 'inset 0 0 0 1px $colors$red8'
}
}
backgroundColor: "$mauve4",
boxShadow: "inset 0 0 0 1px $colors$red8",
},
},
},
muted: {
true: {
color: '$textMuted'
}
color: "$textMuted",
},
},
isDisabled: {
true: {
opacity: 0.6,
// pointerEvents: "none",
cursor: 'auto',
'&:hover': {
boxShadow: 'inherit'
}
}
cursor: "auto",
"&:hover": {
boxShadow: "inherit",
},
},
},
outline: {
true: {
backgroundColor: 'transparent'
}
backgroundColor: "transparent",
},
},
uppercase: {
true: {
textTransform: 'uppercase'
}
textTransform: "uppercase",
},
},
fullWidth: {
true: {
width: '100%'
}
width: "100%",
},
},
ghost: {
true: {
boxShadow: 'none',
background: 'transparent',
color: '$mauve12',
'@hover': {
'&:hover': {
backgroundColor: '$mauve6',
boxShadow: 'none'
}
boxShadow: "none",
background: "transparent",
color: "$mauve12",
"@hover": {
"&:hover": {
backgroundColor: "$mauve6",
boxShadow: "none",
},
},
'&:active': {
backgroundColor: '$mauve8',
boxShadow: 'none'
"&:active": {
backgroundColor: "$mauve8",
boxShadow: "none",
},
'&:focus': {
boxShadow: 'none'
}
}
"&:focus": {
boxShadow: "none",
},
},
},
isLoading: {
true: {
'& .button-content': {
visibility: 'hidden'
"& .button-content": {
visibility: "hidden",
},
pointerEvents: 'none'
}
}
pointerEvents: "none",
},
},
},
compoundVariants: [
{
outline: true,
variant: 'default',
variant: "default",
css: {
background: 'transparent',
color: '$mauve12',
boxShadow: 'inset 0 0 0 1px $colors$mauve10',
'&:hover': {
color: '$mauve12',
background: '$mauve5'
}
}
background: "transparent",
color: "$mauve12",
boxShadow: "inset 0 0 0 1px $colors$mauve10",
"&:hover": {
color: "$mauve12",
background: "$mauve5",
},
},
},
{
outline: true,
variant: 'primary',
variant: "primary",
css: {
background: 'transparent',
color: '$mauve12',
'&:hover': {
color: '$mauve12',
background: '$mauve5'
}
}
background: "transparent",
color: "$mauve12",
"&:hover": {
color: "$mauve12",
background: "$mauve5",
},
},
},
{
outline: true,
variant: 'secondary',
variant: "secondary",
css: {
background: 'transparent',
color: '$mauve12',
'&:hover': {
color: '$mauve12',
background: '$mauve5'
}
}
}
background: "transparent",
color: "$mauve12",
"&:hover": {
color: "$mauve12",
background: "$mauve5",
},
},
},
],
defaultVariants: {
size: 'md',
variant: 'default'
}
})
size: "md",
variant: "default",
},
});
const CustomButton: React.FC<React.ComponentProps<typeof StyledButton> & { as?: string }> =
React.forwardRef(({ children, as = 'button', ...rest }, ref) => (
// @ts-expect-error
<StyledButton {...rest} ref={ref} as={as}>
<Flex as="span" css={{ gap: '$2', alignItems: 'center' }} className="button-content">
{children}
</Flex>
{rest.isLoading && <Spinner css={{ position: 'absolute' }} />}
</StyledButton>
))
const CustomButton: React.FC<
React.ComponentProps<typeof StyledButton> & { as?: string }
> = React.forwardRef(({ children, as = "button", ...rest }, ref) => (
// @ts-expect-error
<StyledButton {...rest} ref={ref} as={as}>
<Flex
as="span"
css={{ gap: "$2", alignItems: "center" }}
className="button-content"
>
{children}
</Flex>
{rest.isLoading && <Spinner css={{ position: "absolute" }} />}
</StyledButton>
));
CustomButton.displayName = 'CustomButton'
CustomButton.displayName = "CustomButton";
export default CustomButton
export default CustomButton;

View File

@@ -1,29 +1,29 @@
import { styled } from '../stitches.config'
import { StyledButton } from './Button'
import { styled } from "../stitches.config";
import { StyledButton } from "./Button";
const ButtonGroup = styled('div', {
display: 'flex',
marginLeft: '1px',
const ButtonGroup = styled("div", {
display: "flex",
marginLeft: "1px",
[`& ${StyledButton}`]: {
marginLeft: '-1px',
px: '$4',
marginLeft: "-1px",
px: "$4",
zIndex: 2,
position: 'relative',
'&:hover, &:focus': {
zIndex: 200
}
position: "relative",
"&:hover, &:focus": {
zIndex: 200,
},
},
[`& ${StyledButton}:not(:only-of-type):not(:first-child):not(:last-child)`]: {
borderRadius: 0
borderRadius: 0,
},
[`& ${StyledButton}:first-child:not(:only-of-type)`]: {
borderBottomRightRadius: 0,
borderTopRightRadius: 0
borderTopRightRadius: 0,
},
[`& ${StyledButton}:last-child:not(:only-of-type)`]: {
borderBottomLeftRadius: 0,
borderTopLeftRadius: 0
}
})
borderTopLeftRadius: 0,
},
});
export default ButtonGroup
export default ButtonGroup;

View File

@@ -1,12 +1,12 @@
import { styled } from '../stitches.config'
import Box from './Box'
import { styled } from "../stitches.config";
import Box from "./Box";
const Container = styled(Box, {
width: '100%',
marginLeft: 'auto',
marginRight: 'auto',
px: '$4',
maxWidth: '100%'
})
width: "100%",
marginLeft: "auto",
marginRight: "auto",
px: "$4",
maxWidth: "100%",
});
export default Container
export default Container;

View File

@@ -1,121 +0,0 @@
import { CaretRight, Check, Circle } from 'phosphor-react'
import { FC, Fragment, ReactNode } from 'react'
import { Flex, Text } from '../'
import {
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuItemIndicator,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuRoot,
ContextMenuSeparator,
ContextMenuTrigger,
ContextMenuTriggerItem
} from './primitive'
export type TextOption = {
type: 'text'
label: ReactNode
onSelect?: () => any
children?: ContentMenuOption[]
}
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 }
export type ContentMenuOption = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
WithCommons
export interface IContextMenu {
options?: ContentMenuOption[]
isNested?: boolean
}
export const ContextMenu: FC<IContextMenu> = ({ children, options, isNested }) => {
return (
<ContextMenuRoot>
{isNested ? (
<ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
) : (
<ContextMenuTrigger>{children}</ContextMenuTrigger>
)}
{options && !!options.length && (
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
{options.map(({ key, ...option }) => {
if (option.type === 'text') {
const { children, label, onSelect } = 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} onSelect={onSelect}>
{label}
</ContextMenuItem>
)
}
if (option.type === 'checkbox') {
const { label, checked, onCheckedChange } = option
return (
<ContextMenuCheckboxItem
key={key}
checked={checked}
onCheckedChange={onCheckedChange}
>
<Flex row align="center">
<ContextMenuItemIndicator>
<Check />
</ContextMenuItemIndicator>
<Text css={{ ml: checked ? '$4' : undefined }}>{label}</Text>
</Flex>
</ContextMenuCheckboxItem>
)
}
if (option.type === 'radio') {
const { label, options, onValueChange, value } = option
return (
<Fragment key={key}>
<ContextMenuLabel>{label}</ContextMenuLabel>
<ContextMenuRadioGroup value={value} onValueChange={onValueChange}>
{options?.map(({ value: v, label }) => {
return (
<ContextMenuRadioItem key={v} value={v}>
<ContextMenuItemIndicator>
<Circle weight="fill" />
</ContextMenuItemIndicator>
<Text css={{ ml: '$4' }}>{label}</Text>
</ContextMenuRadioItem>
)
})}
</ContextMenuRadioGroup>
</Fragment>
)
}
return <ContextMenuSeparator key={key} />
})}
</ContextMenuContent>
)}
</ContextMenuRoot>
)
}
export default ContextMenu

View File

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

View File

@@ -1,119 +1,29 @@
import { useEffect } from 'react'
import ReconnectingWebSocket, { CloseEvent } from 'reconnecting-websocket'
import { proxy, ref, useSnapshot } from 'valtio'
import { subscribeKey } from 'valtio/utils'
import { Select } from '.'
import state, { ILog, transactionsState } from '../state'
import { extractJSON } from '../utils/json'
import EnrichLog from './EnrichLog'
import LogBox from './LogBox'
import { useCallback, useEffect } from "react";
import { proxy, ref, useSnapshot } from "valtio";
import { Select } from ".";
import state, { ILog } from "../state";
import { extractJSON } from "../utils/json";
import LogBox from "./LogBox";
interface ISelect<T = string> {
label: string
value: T
label: string;
value: T;
}
export interface IStreamState {
selectedAccount: ISelect | null
status: 'idle' | 'opened' | 'closed'
statusChangeTimestamp?: number
logs: ILog[]
socket?: ReconnectingWebSocket
}
export const streamState = proxy<IStreamState>({
const streamState = proxy({
selectedAccount: null as ISelect | null,
status: 'idle',
logs: [] as ILog[]
})
const onOpen = (account: ISelect | null) => {
if (!account) {
return
}
// streamState.logs = [];
streamState.status = 'opened'
streamState.statusChangeTimestamp = Date.now()
pushLog(`Debug stream opened for account ${account?.value}`, {
type: 'success'
})
}
const onError = () => {
pushLog('Something went wrong! Check your connection and try again.', {
type: 'error'
})
}
const onClose = (e: CloseEvent) => {
// 999 = closed websocket connection by switching account
if (e.code !== 4999) {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: 'error'
})
}
streamState.status = 'closed'
streamState.statusChangeTimestamp = Date.now()
}
const onMessage = (event: any) => {
// Ping returns just account address, if we get that
// response we don't need to log anything
if (event.data !== streamState.selectedAccount?.value) {
pushLog(event.data)
}
}
let interval: NodeJS.Timer | null = null
const addListeners = (account: ISelect | null) => {
if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
return
}
streamState.logs = []
if (account?.value) {
if (interval) {
clearInterval(interval)
}
if (streamState.socket) {
streamState.socket?.removeEventListener('open', () => onOpen(account))
streamState.socket?.removeEventListener('close', onClose)
streamState.socket?.removeEventListener('error', onError)
streamState.socket?.removeEventListener('message', onMessage)
}
streamState.socket = ref(
new ReconnectingWebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
)
)
if (streamState.socket) {
interval = setInterval(() => {
streamState.socket?.send('')
}, 45000)
}
streamState.socket.addEventListener('open', () => onOpen(account))
streamState.socket.addEventListener('close', onClose)
streamState.socket.addEventListener('error', onError)
streamState.socket.addEventListener('message', onMessage)
}
}
subscribeKey(streamState, 'selectedAccount', addListeners)
const clearLog = () => {
streamState.logs = []
streamState.statusChangeTimestamp = Date.now()
}
logs: [] as ILog[],
socket: undefined as WebSocket | undefined,
});
const DebugStream = () => {
const { selectedAccount, logs } = useSnapshot(streamState)
const { activeHeader: activeTxTab } = useSnapshot(transactionsState)
const { accounts } = useSnapshot(state)
const { selectedAccount, logs, socket } = useSnapshot(streamState);
const { accounts } = useSnapshot(state);
const accountOptions = accounts.map(acc => ({
const accountOptions = accounts.map((acc) => ({
label: acc.name,
value: acc.address
}))
value: acc.address,
}));
const renderNav = () => (
<>
@@ -123,60 +33,115 @@ const DebugStream = () => {
options={accountOptions}
hideSelectedOptions
value={selectedAccount}
onChange={acc => {
streamState.socket?.close(4999, 'Old connection closed because user switched account')
streamState.selectedAccount = acc as any
}}
css={{ width: '100%' }}
onChange={(acc) => (streamState.selectedAccount = acc as any)}
css={{ width: "100%" }}
/>
</>
)
);
const prepareLog = useCallback((str: any): ILog => {
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))\ ?([\s\S]*)/m);
const [_, tm, msg] = match || [];
const extracted = extractJSON(msg);
const timestamp = isNaN(Date.parse(tm || ""))
? tm
: new Date(tm).toLocaleTimeString();
const message = !extracted
? msg
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
const jsonData = extracted
? JSON.stringify(extracted.result, null, 2)
: undefined;
return {
type: "log",
message,
timestamp,
jsonData,
defaultCollapsed: true,
};
}, []);
useEffect(() => {
const account = transactionsState.transactions.find(tx => tx.header === activeTxTab)?.state
.selectedAccount
const account = selectedAccount?.value;
if (account && (!socket || !socket.url.endsWith(account))) {
socket?.close();
streamState.socket = ref(
new WebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account}`
)
);
} else if (!account && socket) {
socket.close();
streamState.socket = undefined;
}
}, [selectedAccount?.value, socket]);
if (account && account.value !== streamState.selectedAccount?.value)
streamState.selectedAccount = account
}, [activeTxTab])
useEffect(() => {
const account = selectedAccount?.value;
const socket = streamState.socket;
if (!socket) return;
const onOpen = () => {
streamState.logs = [];
streamState.logs.push({
type: "success",
message: `Debug stream opened for account ${account}`,
});
};
const onError = () => {
streamState.logs.push({
type: "error",
message: "Something went wrong! Check your connection and try again.",
});
};
const onClose = (e: CloseEvent) => {
streamState.logs.push({
type: "error",
message: `Connection was closed. [code: ${e.code}]`,
});
streamState.selectedAccount = null;
};
const onMessage = (event: any) => {
if (!event.data) return;
const log = prepareLog(event.data);
// Filter out account_info and account_objects requests
try {
const parsed = JSON.parse(log.jsonData);
if (parsed?.id?._Request?.includes("hooks-builder-req")) {
return;
}
} catch (err) {
// Lets just skip if we cannot parse the message
}
return streamState.logs.push(log);
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onClose);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
};
}, [prepareLog, selectedAccount?.value, socket]);
return (
<LogBox enhanced renderNav={renderNav} title="Debug stream" logs={logs} clearLog={clearLog} />
)
}
<LogBox
enhanced
renderNav={renderNav}
title="Debug stream"
logs={logs}
clearLog={() => (streamState.logs = [])}
/>
);
};
export default DebugStream
export const pushLog = (str: any, opts: Partial<Pick<ILog, 'type'>> = {}): ILog | undefined => {
if (!str) return
if (typeof str !== 'string') throw Error('Unrecognized debug log stream!')
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m)
const [_, tm, msg] = match || []
const timestamp = Date.parse(tm || '') || undefined
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString()
const extracted = extractJSON(msg)
const _message = !extracted ? msg : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1)
const message = ref(<EnrichLog str={_message} />)
const _jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined
const jsonData = _jsonData ? ref(<EnrichLog str={_jsonData} />) : undefined
if (extracted?.result?.id?._Request?.includes('hooks-builder-req')) {
return
}
const { type = 'log' } = opts
const log: ILog = {
type,
message,
timestring,
jsonData,
defaultCollapsed: true
}
if (log) streamState.logs.push(log)
return log
}
export default DebugStream;

View File

@@ -1,96 +1,84 @@
import React, { useState } from 'react'
import { useSnapshot } from 'valtio'
import React, { useRef, useState } from "react";
import { useSnapshot, ref } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import NextLink from "next/link";
import ReactTimeAgo from "react-time-ago";
import filesize from "filesize";
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import NextLink from 'next/link'
import ReactTimeAgo from 'react-time-ago'
import filesize from 'filesize'
import Box from "./Box";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import state from "../state";
import wat from "../utils/wat-highlight";
import Box from './Box'
import Container from './Container'
import state from '../state'
import wat from '../utils/wat-highlight'
import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex } from ".";
import EditorNavigation from './EditorNavigation'
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 snap = useSnapshot(state)
const router = useRouter()
const { theme } = useTheme()
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state);
const router = useRouter();
const { theme } = useTheme();
const [showContent, setShowContent] = useState(false)
const [showContent, setShowContent] = useState(false);
const compiledFiles = snap.files.filter(file => file.compiledContent)
const activeFile = compiledFiles[snap.activeWat]
const renderNav = () => (
<Tabs activeIndex={snap.activeWat} onChangeActive={idx => (state.activeWat = idx)}>
{compiledFiles.map((file, index) => {
return <Tab key={file.name} header={`${file.name}.wat`} />
})}
</Tabs>
)
const compiledSize = activeFile?.compiledContent?.byteLength || 0
const activeFile = snap.files[snap.active];
const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color =
compiledSize > FILESIZE_BREAKPOINTS[1]
? '$error'
? "$error"
: compiledSize > FILESIZE_BREAKPOINTS[0]
? '$warning'
: '$success'
const isContentChanged = activeFile && activeFile.compiledValueSnapshot !== activeFile.content
// const hasDeployErrors = activeFile && activeFile.containsErrors;
? "$warning"
: "$success";
const CompiledStatView = activeFile && (
<Flex
column
align="center"
css={{
fontSize: '$sm',
fontFamily: '$monospace',
textAlign: 'center'
fontSize: "$sm",
fontFamily: "$monospace",
textAlign: "center",
}}
>
<Flex row align="center">
<Text css={{ mr: '$1' }}>Compiled {activeFile.name.split('.')[0] + '.wasm'}</Text>
{activeFile?.lastCompiled && <ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />}
<Text css={{ mr: "$1" }}>
Compiled {activeFile.name.split(".")[0] + ".wasm"}
</Text>
{activeFile?.lastCompiled && (
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
)}
{activeFile.compiledContent?.byteLength && (
<Text css={{ ml: '$2', color }}>({filesize(activeFile.compiledContent.byteLength)})</Text>
<Text css={{ ml: "$2", color }}>
({filesize(activeFile.compiledContent.byteLength)})
</Text>
)}
</Flex>
{activeFile.compiledContent?.byteLength && activeFile.compiledContent?.byteLength >= 64000 && (
<Flex css={{ flexDirection: 'column', py: '$3', pb: '$1' }}>
<Text css={{ ml: '$2', color: '$error' }}>
File size is larger than 64kB, cannot set hook!
</Text>
</Flex>
)}
<Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file
</Button>
{isContentChanged && (
<Text warning>
File contents were changed after last compile, compile again to incorporate your latest
changes in the build.
</Text>
)}
</Flex>
)
);
const NoContentView = !snap.loading && router.isReady && (
<Text
css={{
mt: '-60px',
fontSize: '$sm',
fontFamily: '$monospace',
maxWidth: '300px',
textAlign: 'center'
mt: "-60px",
fontSize: "$sm",
fontFamily: "$monospace",
maxWidth: "300px",
textAlign: "center",
}}
>
{`You haven't compiled any files yet, compile files on `}
@@ -98,27 +86,28 @@ const DeployEditor = () => {
<Link as="a">develop view</Link>
</NextLink>
</Text>
)
const isContent = snap.files?.filter(file => file.compiledWatContent).length > 0 && router.isReady
);
const isContent =
snap.files?.filter((file) => file.compiledWatContent).length > 0 &&
router.isReady;
return (
<Box
css={{
flex: 1,
display: 'flex',
position: 'relative',
flexDirection: 'column',
backgroundColor: '$mauve2',
width: '100%'
display: "flex",
position: "relative",
flexDirection: "column",
backgroundColor: "$mauve2",
width: "100%",
}}
>
<EditorNavigation renderNav={renderNav} />
<EditorNavigation showWat />
<Container
css={{
display: 'flex',
display: "flex",
flex: 1,
justifyContent: 'center',
alignItems: 'center'
justifyContent: "center",
alignItems: "center",
}}
>
{!isContent ? (
@@ -126,41 +115,37 @@ const DeployEditor = () => {
) : !showContent ? (
CompiledStatView
) : (
<Monaco
<Editor
className="hooks-editor"
defaultLanguage={'wat'}
language={'wat'}
path={`file://tmp/c/${activeFile?.name}.wat`}
value={activeFile?.compiledWatContent || ''}
beforeMount={monaco => {
monaco.languages.register({ id: 'wat' })
monaco.languages.setLanguageConfiguration('wat', wat.config)
monaco.languages.setMonarchTokensProvider('wat', wat.tokens)
defaultLanguage={"wat"}
language={"wat"}
path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
value={snap.files?.[snap.active]?.compiledWatContent || ""}
beforeMount={(monaco) => {
monaco.languages.register({ id: "wat" });
monaco.languages.setLanguageConfiguration("wat", wat.config);
monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}}
onMount={editor => {
onMount={(editor, monaco) => {
editorRef.current = editor;
editor.updateOptions({
glyphMargin: true,
readOnly: true
})
readOnly: true,
});
}}
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>
}
theme={theme === "dark" ? "dark" : "light"}
/>
)}
</Container>
</Box>
)
}
);
};
export default DeployEditor
export default DeployEditor;

103
components/DeployFooter.tsx Normal file
View File

@@ -0,0 +1,103 @@
import React, { useRef, useLayoutEffect } from "react";
import { useSnapshot } from "valtio";
import { Play, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import Container from "./Container";
import Box from "./Box";
import LogText from "./LogText";
import { compileCode } from "../state/actions";
import state from "../state";
import Button from "./Button";
import Heading from "./Heading";
const Footer = () => {
const snap = useSnapshot(state);
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
useLayoutEffect(() => {
stayScrolled();
}, [snap.logs, stayScrolled]);
return (
<Box
as="footer"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
}}
>
<Container css={{ py: "$3", flexShrink: 1 }}>
<Heading
as="h3"
css={{ fontWeight: 300, m: 0, fontSize: "11px", color: "$mauve9" }}
>
DEVELOPMENT LOG
</Heading>
<Button
ghost
size="xs"
css={{
position: "absolute",
right: "$3",
top: "$2",
color: "$mauve10",
}}
onClick={() => {
state.logs = [];
}}
>
<Prohibit size="14px" />
</Button>
<Box
as="pre"
ref={logRef}
css={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
overflowY: "auto",
wordWrap: "break-word",
py: 3,
}}
>
{snap.logs?.map((log, index) => (
<Box as="span" key={log.type + index}>
<LogText capitalize variant={log.type}>
{log.type}:{" "}
</LogText>
<LogText>{log.message}</LogText>
</Box>
))}
</Box>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Container>
</Box>
);
};
export default Footer;

View File

@@ -1,88 +1,89 @@
import React from 'react'
import * as Stiches from '@stitches/react'
import { keyframes } from '@stitches/react'
import { blackA } from '@radix-ui/colors'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { styled } from '../stitches.config'
import React from "react";
import * as Stiches from "@stitches/react";
import { keyframes } from "@stitches/react";
import { blackA } from "@radix-ui/colors";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { styled } from "../stitches.config";
const overlayShow = keyframes({
'0%': { opacity: 0.01 },
'100%': { opacity: 1 }
})
"0%": { opacity: 0.01 },
"100%": { opacity: 1 },
});
const contentShow = keyframes({
'0%': { opacity: 0.01 },
'100%': { opacity: 1 }
})
"0%": { opacity: 0.01 },
"100%": { opacity: 1 },
});
const StyledOverlay = styled(DialogPrimitive.Overlay, {
zIndex: 10000,
zIndex: 9999,
backgroundColor: blackA.blackA9,
position: 'fixed',
position: "fixed",
inset: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'grid',
placeItems: 'center',
overflowY: 'auto',
'@media (prefers-reduced-motion: no-preference)': {
animation: `${overlayShow} 250ms cubic-bezier(0.16, 1, 0.3, 1)`
display: "grid",
placeItems: "center",
overflowY: "auto",
"@media (prefers-reduced-motion: no-preference)": {
animation: `${overlayShow} 250ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
'.dark &': {
backgroundColor: blackA.blackA11
}
})
".dark &": {
backgroundColor: blackA.blackA11,
},
});
const StyledContent = styled(DialogPrimitive.Content, {
zIndex: 1000,
backgroundColor: '$mauve2',
color: '$mauve12',
borderRadius: '$md',
position: 'relative',
mb: '15%',
boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)',
width: '90vw',
maxWidth: '450px',
backgroundColor: "$mauve2",
color: "$mauve12",
borderRadius: "$md",
position: "relative",
boxShadow:
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
width: "90vw",
maxWidth: "450px",
// maxHeight: "85vh",
padding: 25,
'@media (prefers-reduced-motion: no-preference)': {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
"@media (prefers-reduced-motion: no-preference)": {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
'&:focus': { outline: 'none' },
'.dark &': {
backgroundColor: '$mauve5',
boxShadow: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)'
}
})
"&:focus": { outline: "none" },
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
"0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
},
});
const Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => {
return (
<StyledOverlay>
<StyledContent css={css}>{children}</StyledContent>
</StyledOverlay>
)
}
);
};
const StyledTitle = styled(DialogPrimitive.Title, {
margin: 0,
fontWeight: 500,
color: '$mauve12',
fontSize: 17
})
color: "$mauve12",
fontSize: 17,
});
const StyledDescription = styled(DialogPrimitive.Description, {
margin: '10px 0 10px',
color: '$mauve11',
margin: "10px 0 10px",
color: "$mauve11",
fontSize: 15,
lineHeight: 1.5
})
lineHeight: 1.5,
});
// Exports
export const Dialog = styled(DialogPrimitive.Root)
export const DialogTrigger = DialogPrimitive.Trigger
export const DialogContent = Content
export const DialogTitle = StyledTitle
export const DialogDescription = StyledDescription
export const DialogClose = DialogPrimitive.Close
export const DialogPortal = DialogPrimitive.Portal
export const Dialog = styled(DialogPrimitive.Root);
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogContent = Content;
export const DialogTitle = StyledTitle;
export const DialogDescription = StyledDescription;
export const DialogClose = DialogPrimitive.Close;
export const DialogPortal = DialogPrimitive.Portal;

View File

@@ -1,120 +1,135 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { keyframes } from "@stitches/react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { styled } from '../stitches.config'
import {
slideDownAndFade,
slideLeftAndFade,
slideRightAndFade,
slideUpAndFade
} from '../styles/keyframes'
import { styled } from "../stitches.config";
const slideUpAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideRightAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(-2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const slideDownAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(-2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideLeftAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const StyledContent = styled(DropdownMenuPrimitive.Content, {
minWidth: 220,
backgroundColor: '$mauve2',
backgroundColor: "$mauve2",
borderRadius: 6,
padding: 5,
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',
"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 }
}
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
'.dark &': {
backgroundColor: '$mauve5',
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
'0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)'
}
})
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
},
});
const itemStyles = {
all: 'unset',
all: "unset",
fontSize: 13,
lineHeight: 1,
color: '$mauve12',
color: "$mauve12",
borderRadius: 3,
display: 'flex',
alignItems: 'center',
display: "flex",
alignItems: "center",
height: 32,
padding: '0 5px',
position: 'relative',
paddingLeft: '5px',
userSelect: 'none',
py: '$0.5',
pr: '$2',
gap: '$2',
padding: "0 5px",
position: "relative",
paddingLeft: "5px",
userSelect: "none",
py: "$0.5",
pr: "$2",
gap: "$2",
'&[data-disabled]': {
color: '$mauve9',
pointerEvents: 'none'
"&[data-disabled]": {
color: "$mauve9",
pointerEvents: "none",
},
'&:focus': {
backgroundColor: '$purple9',
color: '$white'
}
}
"&:focus": {
backgroundColor: "$purple9",
color: "$white",
},
};
const StyledItem = styled(DropdownMenuPrimitive.Item, { ...itemStyles })
const StyledItem = styled(DropdownMenuPrimitive.Item, { ...itemStyles });
const StyledCheckboxItem = styled(DropdownMenuPrimitive.CheckboxItem, {
...itemStyles
})
...itemStyles,
});
const StyledRadioItem = styled(DropdownMenuPrimitive.RadioItem, {
...itemStyles
})
...itemStyles,
});
const StyledTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
'&[data-state="open"]': {
backgroundColor: '$purple9',
color: '$purple9'
backgroundColor: "$purple9",
color: "$purple9",
},
...itemStyles
})
...itemStyles,
});
const StyledLabel = styled(DropdownMenuPrimitive.Label, {
paddingLeft: 25,
fontSize: 12,
lineHeight: '25px',
color: '$mauve11'
})
lineHeight: "25px",
color: "$mauve11",
});
const StyledSeparator = styled(DropdownMenuPrimitive.Separator, {
height: 1,
backgroundColor: '$mauve7',
margin: 5
})
backgroundColor: "$mauve7",
margin: 5,
});
const StyledItemIndicator = styled(DropdownMenuPrimitive.ItemIndicator, {
position: 'absolute',
position: "absolute",
left: 0,
width: 25,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center'
})
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
});
const StyledArrow = styled(DropdownMenuPrimitive.Arrow, {
fill: '$mauve2',
'.dark &': {
fill: '$mauve5'
}
})
fill: "$mauve2",
".dark &": {
fill: "$mauve5",
},
});
// Exports
export const DropdownMenu = DropdownMenuPrimitive.Root
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
export const DropdownMenuContent = StyledContent
export const DropdownMenuItem = StyledItem
export const DropdownMenuCheckboxItem = StyledCheckboxItem
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
export const DropdownMenuRadioItem = StyledRadioItem
export const DropdownMenuItemIndicator = StyledItemIndicator
export const DropdownMenuTriggerItem = StyledTriggerItem
export const DropdownMenuLabel = StyledLabel
export const DropdownMenuSeparator = StyledSeparator
export const DropdownMenuArrow = StyledArrow
export const DropdownMenu = DropdownMenuPrimitive.Root;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
export const DropdownMenuContent = StyledContent;
export const DropdownMenuItem = StyledItem;
export const DropdownMenuCheckboxItem = StyledCheckboxItem;
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
export const DropdownMenuRadioItem = StyledRadioItem;
export const DropdownMenuItemIndicator = StyledItemIndicator;
export const DropdownMenuTriggerItem = StyledTriggerItem;
export const DropdownMenuLabel = StyledLabel;
export const DropdownMenuSeparator = StyledSeparator;
export const DropdownMenuArrow = StyledArrow;

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef, ReactNode } from 'react'
import React, { useState, useEffect, useCallback } from "react";
import {
Plus,
Share,
DownloadSimple,
Gear,
@@ -10,151 +11,288 @@ import {
CloudArrowUp,
CaretDown,
User,
FilePlus
} from 'phosphor-react'
import Image from 'next/image'
FilePlus,
} from "phosphor-react";
import Image from "next/image";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuArrow,
DropdownMenuSeparator
} from './DropdownMenu'
import NewWindow from 'react-new-window'
import { signOut, useSession } from 'next-auth/react'
import { useSnapshot } from 'valtio'
import toast from 'react-hot-toast'
DropdownMenuSeparator,
} from "./DropdownMenu";
import NewWindow from "react-new-window";
import { signOut, useSession } from "next-auth/react";
import { useSnapshot } from "valtio";
import toast from "react-hot-toast";
import { syncToGist, updateEditorSettings, downloadAsZip } from '../state/actions'
import state from '../state'
import Box from './Box'
import Button from './Button'
import Container from './Container'
import {
createNewFile,
syncToGist,
updateEditorSettings,
downloadAsZip,
} from "../state/actions";
import state from "../state";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose
} from './Dialog'
import Flex from './Flex'
import Stack from './Stack'
import { Input, Label } from './Input'
import Tooltip from './Tooltip'
import { showAlert } from '../state/actions/showAlert'
DialogClose,
} from "./Dialog";
import Flex from "./Flex";
import Stack from "./Stack";
import Input from "./Input";
import Text from "./Text";
import Tooltip from "./Tooltip";
import {
AlertDialog,
AlertDialogContent,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogCancel,
AlertDialogAction,
} from "./AlertDialog";
import { styled } from "../stitches.config";
const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
const snap = useSnapshot(state)
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false)
const { data: session, status } = useSession()
const [popup, setPopUp] = useState(false)
const [editorSettings, setEditorSettings] = useState(snap.editorSettings)
const ErrorText = styled(Text, {
color: "$error",
mt: "$1",
display: "block",
});
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
const snap = useSnapshot(state);
const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null);
const [filename, setFilename] = useState("");
const { data: session, status } = useSession();
const [popup, setPopUp] = useState(false);
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
useEffect(() => {
if (session && session.user && popup) {
setPopUp(false)
setPopUp(false);
}
}, [session, popup])
}, [session, popup]);
const showNewGistAlert = () => {
showAlert('Are you sure?', {
body: (
<>
This action will create new <strong>public</strong> Github Gist from your current saved
files. You can delete gist anytime from your GitHub Gists page.
</>
),
cancelText: 'Cancel',
confirmText: 'Create new Gist',
confirmPrefix: <FilePlus size="15px" />,
onConfirm: () => syncToGist(session, true)
})
}
// when filename changes, reset error
useEffect(() => {
setNewfileError(null);
}, [filename, setNewfileError]);
const scrollRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
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 files = snap.files;
return (
<Flex css={{ flexShrink: 0, gap: '$0' }}>
<Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex
id="kissa"
ref={scrollRef}
css={{
overflowX: 'scroll',
overflowY: 'hidden',
py: '$3',
pb: '$0',
overflowX: "scroll",
py: "$3",
flex: 1,
'&::-webkit-scrollbar': {
height: '0.3em',
background: 'rgba(0,0,0,.0)'
"&::-webkit-scrollbar": {
height: 0,
background: "transparent",
},
'&::-webkit-scrollbar-gutter': 'stable',
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(0,0,0,.2)',
outline: '0px',
borderRadius: '9999px'
},
scrollbarColor: 'rgba(0,0,0,.2) rgba(0,0,0,0)',
scrollbarGutter: 'stable',
scrollbarWidth: 'thin',
'.dark &': {
'&::-webkit-scrollbar': {
background: 'rgba(0,0,0,.0)'
},
'&::-webkit-scrollbar-gutter': 'stable',
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(255,255,255,.2)',
outline: '0px',
borderRadius: '9999px'
},
scrollbarColor: 'rgba(255,255,255,.2) rgba(0,0,0,0)',
scrollbarGutter: 'stable',
scrollbarWidth: 'thin'
}
}}
onWheelCapture={e => {
if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY
}
}}
>
<Container css={{ flex: 1 }} ref={containerRef}>
{renderNav?.()}
<Container css={{ flex: 1 }}>
<Stack
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
}}
>
{files &&
files.length > 0 &&
files.map((file, index) => {
if (!file.compiledContent && showWat) {
return null;
}
return (
<Button
size="sm"
outline={
showWat ? snap.activeWat !== index : snap.active !== index
}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{file.name}
{showWat && ".wat"}
{!showWat && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active =
index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
);
})}
{!showWat && (
<Dialog
open={isNewfileDialogOpen}
onOpenChange={setIsNewfileDialogOpen}
>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<label>Filename</label>
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleConfirm();
}
}}
/>
<ErrorText>{newfileError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleConfirm}>
Create file
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
</Stack>
</Container>
</Flex>
<Flex
css={{
py: '$3',
backgroundColor: '$mauve2',
zIndex: 1
py: "$3",
backgroundColor: "$mauve2",
zIndex: 1,
}}
>
<Container css={{ width: 'unset', display: 'flex', alignItems: 'center' }}>
{status === 'authenticated' ? (
<Container
css={{ width: "unset", display: "flex", alignItems: "center" }}
>
{status === "authenticated" ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Box
css={{
display: 'flex',
borderRadius: '$full',
overflow: 'hidden',
width: '$6',
height: '$6',
boxShadow: '0px 0px 0px 1px $colors$mauve11',
position: 'relative',
mr: '$3',
'@hover': {
'&:hover': {
cursor: 'pointer',
boxShadow: '0px 0px 0px 1px $colors$mauve12'
}
}
display: "flex",
borderRadius: "$full",
overflow: "hidden",
width: "$6",
height: "$6",
boxShadow: "0px 0px 0px 1px $colors$mauve11",
position: "relative",
mr: "$3",
"@hover": {
"&:hover": {
cursor: "pointer",
boxShadow: "0px 0px 0px 1px $colors$mauve12",
},
},
}}
>
<Image
src={session?.user?.image || ''}
src={session?.user?.image || ""}
width="30px"
height="30px"
objectFit="cover"
@@ -164,16 +302,21 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem disabled onClick={() => signOut()}>
<User size="16px" /> {session?.user?.username} ({session?.user.name})
<User size="16px" /> {session?.user?.username} (
{session?.user.name})
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => window.open(`http://gist.github.com/${session?.user.username}`)}
onClick={() =>
window.open(
`http://gist.github.com/${session?.user.username}`
)
}
>
<ArrowSquareOut size="16px" />
Go to your Gist
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut({ callbackUrl: '/' })}>
<DropdownMenuItem onClick={() => signOut({ callbackUrl: "/" })}>
<SignOut size="16px" /> Log out
</DropdownMenuItem>
@@ -181,43 +324,48 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button outline size="sm" css={{ mr: '$3' }} onClick={() => setPopUp(true)}>
<Button
outline
size="sm"
css={{ mr: "$3" }}
onClick={() => setPopUp(true)}
>
<GithubLogo size="16px" /> Login
</Button>
)}
<Stack
css={{
display: 'inline-flex',
marginLeft: 'auto',
display: "inline-flex",
marginLeft: "auto",
flexShrink: 0,
gap: '$0',
borderRadius: '$sm',
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
gap: "$0",
borderRadius: "$sm",
boxShadow: "inset 0px 0px 0px 1px $colors$mauve10",
zIndex: 9,
position: 'relative',
position: "relative",
button: {
borderRadius: 0,
px: '$2',
alignSelf: 'flex-start',
boxShadow: 'none'
px: "$2",
alignSelf: "flex-start",
boxShadow: "none",
},
'button:not(:first-child):not(:last-child)': {
"button:not(:first-child):not(:last-child)": {
borderRight: 0,
borderLeft: 0
borderLeft: 0,
},
'button:first-child': {
borderTopLeftRadius: '$sm',
borderBottomLeftRadius: '$sm'
"button:first-child": {
borderTopLeftRadius: "$sm",
borderBottomLeftRadius: "$sm",
},
"button:last-child": {
borderTopRightRadius: "$sm",
borderBottomRightRadius: "$sm",
boxShadow: "inset 0px 0px 0px 1px $colors$mauve10",
"&:hover": {
boxShadow: "inset 0px 0px 0px 1px $colors$mauve12",
},
},
'button:last-child': {
borderTopRightRadius: '$sm',
borderBottomRightRadius: '$sm',
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
'&:hover': {
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve12'
}
}
}}
>
<Tooltip content="Download as ZIP">
@@ -226,7 +374,7 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
onClick={downloadAsZip}
outline
size="sm"
css={{ alignItems: 'center' }}
css={{ alignItems: "center" }}
>
<DownloadSimple size="16px" />
</Button>
@@ -235,10 +383,12 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
<Button
outline
size="sm"
css={{ alignItems: 'center' }}
css={{ alignItems: "center" }}
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/develop/${snap.gistId}`)
toast.success('Copied share link to clipboard!')
navigator.clipboard.writeText(
`${window.location.origin}/develop/${snap.gistId}`
);
toast.success("Copied share link to clipboard!");
}}
>
<Share size="16px" />
@@ -248,9 +398,9 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
content={
session && session.user
? snap.gistOwner === session?.user.username
? 'Sync to Gist'
: 'Create as a new Gist'
: 'You need to be logged in to sync with Gist'
? "Sync to Gist"
: "Create as a new Gist"
: "You need to be logged in to sync with Gist"
}
>
<Button
@@ -258,15 +408,15 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
size="sm"
isDisabled={!session || !session.user}
isLoading={snap.gistLoading}
css={{ alignItems: 'center' }}
css={{ alignItems: "center" }}
onClick={() => {
if (!session || !session.user) {
return
return;
}
if (snap.gistOwner === session?.user.username) {
syncToGist(session)
syncToGist(session);
} else {
showNewGistAlert()
setCreateNewAlertOpen(true);
}
}}
>
@@ -285,33 +435,38 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem disabled={snap.zipLoading} onClick={downloadAsZip}>
<DropdownMenuItem
disabled={snap.zipLoading}
onClick={downloadAsZip}
>
<DownloadSimple size="16px" /> Download as ZIP
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/develop/${snap.gistId}`
)
toast.success('Copied share link to clipboard!')
);
toast.success("Copied share link to clipboard!");
}}
>
<Share size="16px" />
Copy share link to clipboard
</DropdownMenuItem>
<DropdownMenuItem
disabled={session?.user.username !== snap.gistOwner || !snap.gistId}
disabled={
session?.user.username !== snap.gistOwner || !snap.gistId
}
onClick={() => {
syncToGist(session)
syncToGist(session);
}}
>
<CloudArrowUp size="16px" /> Push to Gist
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={status !== 'authenticated'}
disabled={status !== "authenticated"}
onClick={() => {
showNewGistAlert()
setCreateNewAlertOpen(true);
}}
>
<FilePlus size="16px" /> Create as a new Gist
@@ -326,9 +481,39 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</DropdownMenu>
</Stack>
{popup && !session ? <NewWindow center="parent" url="/sign-in" /> : null}
{popup && !session ? (
<NewWindow center="parent" url="/sign-in" />
) : null}
</Container>
</Flex>
<AlertDialog
open={createNewAlertOpen}
onOpenChange={(value) => setCreateNewAlertOpen(value)}
>
<AlertDialogContent>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action will create new <strong>public</strong> Github Gist from
your current saved files. You can delete gist anytime from your
GitHub Gists page.
</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
<AlertDialogCancel asChild>
<Button outline>Cancel</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
variant="primary"
onClick={() => {
syncToGist(session, true);
}}
>
<FilePlus size="15px" /> Create new Gist
</Button>
</AlertDialogAction>
</Flex>
</AlertDialogContent>
</AlertDialog>
<Dialog open={editorSettingsOpen} onOpenChange={setEditorSettingsOpen}>
<DialogTrigger asChild>
@@ -339,41 +524,47 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
<DialogContent>
<DialogTitle>Editor settings</DialogTitle>
<DialogDescription>
<Label>Tab size</Label>
<label>Tab size</label>
<Input
type="number"
min="1"
value={editorSettings.tabSize}
onChange={e =>
setEditorSettings(curr => ({
onChange={(e) =>
setEditorSettings((curr) => ({
...curr,
tabSize: Number(e.target.value)
tabSize: Number(e.target.value),
}))
}
/>
</DialogDescription>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end', gap: '$3' }}>
<Flex css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}>
<DialogClose asChild>
<Button outline onClick={() => updateEditorSettings(editorSettings)}>
<Button
outline
onClick={() => updateEditorSettings(editorSettings)}
>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="primary" onClick={() => updateEditorSettings(editorSettings)}>
<Button
variant="primary"
onClick={() => updateEditorSettings(editorSettings)}
>
Save changes
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
</Flex>
)
}
);
};
export default EditorNavigation
export default EditorNavigation;

View File

@@ -1,73 +0,0 @@
import { FC, useState } from 'react'
import regexifyString from 'regexify-string'
import { useSnapshot } from 'valtio'
import { Link } from '.'
import state from '../state'
import { AccountDialog } from './Accounts'
import Tooltip from './Tooltip'
import hookSetCodes from '../content/hook-set-codes.json'
import { capitalize } from '../utils/helpers'
interface EnrichLogProps {
str?: string
}
const EnrichLog: FC<EnrichLogProps> = ({ str }) => {
const { accounts } = useSnapshot(state)
const [dialogAccount, setDialogAccount] = useState<string | null>(null)
if (!str || !accounts.length) return <>{str}</>
const addrs = accounts.map(acc => acc.address)
const regex = `(${addrs.join('|')}|HookSet\\(\\d+\\))`
const res = regexifyString({
pattern: new RegExp(regex, 'gim'),
decorator: (match, idx) => {
if (match.startsWith('r')) {
// Account
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>
)
}
if (match.startsWith('HookSet')) {
const code = match.match(/^HookSet\((\d+)\)/)?.[1]
const val = hookSetCodes.find(v => code && v.code === +code)
console.log({ code, val })
if (!val) return match
const content = capitalize(val.description) || 'No hint available!'
return (
<>
HookSet(
<Tooltip content={content}>
<Link>{val.identifier}</Link>
</Tooltip>
)
</>
)
}
return match
},
input: str
})
return (
<>
{res}
<AccountDialog
setActiveAccountAddress={setDialogAccount}
activeAccountAddress={dialogAccount}
/>
</>
)
}
export default EnrichLog

View File

@@ -1,53 +1,53 @@
import { styled } from '../stitches.config'
import Box from './Box'
import { styled } from "../stitches.config";
import Box from "./Box";
const Flex = styled(Box, {
display: 'flex',
display: "flex",
variants: {
row: {
true: {
flexDirection: 'row'
}
flexDirection: "row",
},
},
column: {
true: {
flexDirection: 'column'
}
flexDirection: "column",
},
},
fluid: {
true: {
width: '100%'
}
width: "100%",
},
},
align: {
start: {
alignItems: 'start'
alignItems: "start",
},
center: {
alignItems: 'center'
alignItems: "center",
},
end: {
alignItems: 'end'
}
alignItems: "end",
},
},
justify: {
start: {
justifyContent: 'start'
justifyContent: "start",
},
center: {
justifyContent: 'center'
justifyContent: "center",
},
end: {
justifyContent: 'end'
justifyContent: "end",
},
'space-between': {
justifyContent: 'space-between'
"space-between": {
justifyContent: "space-between",
},
'space-around': {
justifyContent: 'space-around'
}
}
}
})
"space-around": {
justifyContent: "space-around",
},
},
},
});
export default Flex
export default Flex;

View File

@@ -1,16 +1,16 @@
import { styled } from '../stitches.config'
import { styled } from "../stitches.config";
const Heading = styled('span', {
fontFamily: '$heading',
lineHeight: '$heading',
fontWeight: '$heading',
const Heading = styled("span", {
fontFamily: "$heading",
lineHeight: "$heading",
fontWeight: "$heading",
variants: {
uppercase: {
true: {
textTransform: 'uppercase'
}
}
}
})
textTransform: "uppercase",
},
},
},
});
export default Heading
export default Heading;

View File

@@ -1,59 +1,63 @@
import React, { useEffect, useRef, useState } from 'react'
import { useSnapshot, ref } from 'valtio'
import type monaco from 'monaco-editor'
import { ArrowBendLeftUp } from 'phosphor-react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import { listen } from "@codingame/monaco-jsonrpc";
import { MonacoServices } from "@codingame/monaco-languageclient";
import Editor, { loader } from "@monaco-editor/react";
import uniqBy from "lodash.uniqby";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import { ArrowBendLeftUp } from "phosphor-react";
import React, { useEffect, useRef } from "react";
import toast from "react-hot-toast";
import ReconnectingWebSocket from "reconnecting-websocket";
import { ref, useSnapshot } from "valtio";
import state from "../state";
import { saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { createLanguageClient, createWebSocket } from "../utils/languageClient";
import docs from "../xrpl-hooks-docs/docs";
import Box from "./Box";
import Container from "./Container";
import EditorNavigation from "./EditorNavigation";
import Text from "./Text";
import Box from './Box'
import Container from './Container'
import { createNewFile, saveFile } from '../state/actions'
import { apiHeaderFiles } from '../state/constants'
import state from '../state'
import EditorNavigation from './EditorNavigation'
import Text from './Text'
import { MonacoServices } from '@codingame/monaco-languageclient'
import { createLanguageClient, createWebSocket } from '../utils/languageClient'
import { listen } from '@codingame/monaco-jsonrpc'
import ReconnectingWebSocket from 'reconnecting-websocket'
import docs from '../xrpl-hooks-docs/docs'
import Monaco from './Monaco'
import { saveAllFiles } from '../state/actions/saveFile'
import { Tab, Tabs } from './Tabs'
import { renameFile } from '../state/actions/createNewFile'
import { Link } from '.'
import Markdown from './Markdown'
const checkWritable = (filename?: string): boolean => {
if (apiHeaderFiles.find(file => file === filename)) {
return false
}
return true
}
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const filename = editor.getModel()?.uri.path.split('/').pop()
const isWritable = checkWritable(filename)
editor.updateOptions({ readOnly: !isWritable })
}
const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true });
} else {
editor.updateOptions({ readOnly: false });
}
};
let decorations: { [key: string]: string[] } = {}
let decorations: { [key: string]: string[] } = {};
const setMarkers = (monacoE: typeof monaco) => {
// Get all the markers that are active at the moment,
// Also if same error is there twice, we can show the content
// only once (that's why we're using uniqBy)
const markers = monacoE.editor
.getModelMarkers({})
// Filter out the markers that are hooks specific
.filter(
marker =>
typeof marker?.code === 'string' &&
// Take only markers that starts with "hooks-"
marker?.code?.includes('hooks-')
)
const markers = uniqBy(
monacoE.editor
.getModelMarkers({})
// Filter out the markers that are hooks specific
.filter(
(marker) =>
typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-")
),
"code"
);
// Get the active model (aka active file you're editing)
// const model = monacoE.editor?.getModel(
@@ -62,15 +66,17 @@ const setMarkers = (monacoE: typeof monaco) => {
// console.log(state.active);
// Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are
const models = monacoE.editor.getModels()
models.forEach(model => {
const models = monacoE.editor.getModels();
models.forEach((model) => {
decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [],
markers
.filter(marker =>
marker?.resource.path.split('/').includes(`${state.files?.[state.active]?.name}`)
.filter((marker) =>
marker?.resource.path
.split("/")
.includes(`${state.files?.[state.active]?.name}`)
)
.map(marker => ({
.map((marker) => ({
range: new monacoE.Range(
marker.startLineNumber,
marker.startColumn,
@@ -84,239 +90,176 @@ const setMarkers = (monacoE: typeof monaco) => {
// /xrpl-hooks-docs/xrpl-hooks-docs-files.json file
// which was generated from rst files
(typeof marker.code === 'string' && docs[marker?.code]?.toString()) || '',
(typeof marker.code === "string" &&
docs[marker?.code]?.toString()) ||
"",
supportHtml: true,
isTrusted: true
}
}
isTrusted: true,
},
},
}))
)
})
}
);
});
};
const HooksEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
const monacoRef = useRef<typeof monaco>()
const subscriptionRef = useRef<ReconnectingWebSocket | null>(null)
const snap = useSnapshot(state)
const router = useRouter()
const { theme } = useTheme()
const [isMdPreview, setIsMdPreview] = useState(true)
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const monacoRef = useRef<typeof monaco>();
const subscriptionRef = useRef<ReconnectingWebSocket | null>(null);
const snap = useSnapshot(state);
const router = useRouter();
const { theme } = useTheme();
useEffect(() => {
if (editorRef.current) validateWritability(editorRef.current)
}, [snap.active])
if (editorRef.current) validateWritability(editorRef.current);
}, [snap.active]);
useEffect(() => {
return () => {
subscriptionRef?.current?.close()
}
}, [])
subscriptionRef?.current?.close();
};
}, []);
useEffect(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current)
setMarkers(monacoRef.current);
}
}, [snap.active])
useEffect(() => {
return () => {
saveAllFiles()
}
}, [])
}, [snap.active]);
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)}
onRenameTab={(idx, nwName, oldName = '') => renameFile(oldName, nwName)}
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} renameDisabled={!checkWritable(file.name)} />
})}
</Tabs>
)
const previewToggle = (
<Link
onClick={() => {
if (!isMdPreview) {
saveFile(false)
}
setIsMdPreview(!isMdPreview)
}}
css={{
position: 'absolute',
right: 0,
bottom: 0,
zIndex: 10,
m: '$1',
fontSize: '$sm'
}}
>
{isMdPreview ? 'Exit Preview' : 'View Preview'}
</Link>
)
return (
<Box
css={{
flex: 1,
flexShrink: 1,
display: 'flex',
position: 'relative',
flexDirection: 'column',
backgroundColor: '$mauve2',
width: '100%'
display: "flex",
position: "relative",
flexDirection: "column",
backgroundColor: "$mauve2",
width: "100%",
}}
>
<EditorNavigation renderNav={renderNav} />
{file?.language === 'markdown' && previewToggle}
<EditorNavigation />
{snap.files.length > 0 && router.isReady ? (
isMdPreview && file?.language === 'markdown' ? (
<Markdown
components={{
a: ({ href, children }) => (
<Link target="_blank" rel="noopener noreferrer" href={href}>
{children}
</Link>
)
}}
>
{file.content}
</Markdown>
) : (
<Monaco
keepCurrentModel
defaultLanguage={file?.language}
language={file?.language}
path={`file:///work/c/${file?.name}`}
defaultValue={file?.content}
// onChange={val => (state.files[snap.active].content = val)} // Auto save?
beforeMount={monaco => {
// if (!snap.editorCtx) {
// snap.files.forEach(file =>
// monaco.editor.createModel(
// file.content,
// file.language,
// monaco.Uri.parse(`file:///work/c/${file.name}`)
// )
// )
// }
// create the web socket
if (!subscriptionRef.current) {
monaco.languages.register({
id: 'c',
extensions: ['.c', '.h'],
aliases: ['C', 'c', 'H', 'h'],
mimetypes: ['text/plain']
})
monaco.languages.register({
id: 'text',
extensions: ['.txt'],
mimetypes: ['text/plain'],
})
MonacoServices.install(monaco)
const webSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ''
<Editor
className="hooks-editor"
keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language}
language={snap.files?.[snap.active]?.language}
path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
defaultValue={snap.files?.[snap.active]?.content}
beforeMount={(monaco) => {
if (!snap.editorCtx) {
snap.files.forEach((file) =>
monaco.editor.createModel(
file.content,
file.language,
monaco.Uri.parse(`file:///work/c/${file.name}`)
)
subscriptionRef.current = webSocket
// listen when the web socket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: connection => {
// create and start the language client
const languageClient = createLanguageClient(connection)
const disposable = languageClient.start()
);
}
connection.onClose(() => {
try {
disposable.dispose()
} catch (err) {
console.log('err', err)
}
})
}
})
// create the websocket
if (!subscriptionRef.current) {
monaco.languages.register({
id: "c",
extensions: [".c", ".h"],
aliases: ["C", "c", "H", "h"],
mimetypes: ["text/plain"],
});
MonacoServices.install(monaco);
const webSocket: ReconnectingWebSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ""
);
subscriptionRef.current = webSocket;
// listen when the websocket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: (connection) => {
// create and start the language client
const languageClient = createLanguageClient(connection);
const disposable = languageClient.start();
connection.onClose(() => {
try {
// disposable.stop();
disposable.dispose();
} catch (err) {
toast.error('Connection to language server lost!')
console.error("Couldn't dispose the language server connection! ", err);
}
});
connection.onDispose(() => {
toast.error('Connection to language server lost!')
})
// TODO: Check if we need to listen to more connection events
},
});
}
// // hook editor to global state
// editor.updateOptions({
// minimap: {
// enabled: false,
// },
// ...snap.editorSettings,
// });
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) => {
editorRef.current = editor;
monacoRef.current = monaco;
editor.updateOptions({
glyphMargin: true,
lightbulb: {
enabled: true,
},
});
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
() => {
saveFile();
}
// editor.updateOptions({
// minimap: {
// enabled: false,
// },
// ...snap.editorSettings,
// });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor)
);
// When the markers (errors/warnings from clangd language server) change
// Lets improve the markers by adding extra content to them from related
// md files
monaco.editor.onDidChangeMarkers(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current);
}
}}
onMount={(editor, monaco) => {
editorRef.current = editor
monacoRef.current = monaco
editor.updateOptions({
glyphMargin: true,
lightbulb: {
enabled: true
}
})
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
saveFile()
})
// When the markers (errors/warnings from clangd language server) change
// Lets improve the markers by adding extra content to them from related
// md files
monaco.editor.onDidChangeMarkers(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current)
}
})
});
// Hacky way to hide Peek menu
editor.onContextMenu(e => {
const host = document.querySelector<HTMLElement>('.shadow-root-host')
const contextMenuItems = host?.shadowRoot?.querySelectorAll('li.action-item')
contextMenuItems?.forEach(k => {
// If menu item contains "Peek" lets hide it
if (k.querySelector('.action-label')?.textContent === 'Peek') {
// @ts-expect-error
k['style'].display = 'none'
}
})
})
validateWritability(editor)
}}
theme={theme === 'dark' ? 'dark' : 'light'}
/>
)
validateWritability(editor);
}}
theme={theme === "dark" ? "dark" : "light"}
/>
) : (
<Container>
{!snap.loading && router.isReady && (
<Box
css={{
flexDirection: 'row',
width: '$spaces$wide',
gap: '$3',
display: 'inline-flex'
flexDirection: "row",
width: "$spaces$wide",
gap: "$3",
display: "inline-flex",
}}
>
<Box css={{ display: 'inline-flex', pl: '35px' }}>
<Box css={{ display: "inline-flex", pl: "35px" }}>
<ArrowBendLeftUp size={30} />
</Box>
<Box css={{ pl: '0px', pt: '15px', flex: 1, display: 'inline-flex' }}>
<Box
css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}
>
<Text
css={{
fontSize: '14px',
maxWidth: '220px',
fontFamily: '$monospace'
fontSize: "14px",
maxWidth: "220px",
fontFamily: "$monospace",
}}
>
Click the link above to create your file
@@ -327,7 +270,7 @@ const HooksEditor = () => {
</Container>
)}
</Box>
)
}
);
};
export default HooksEditor
export default HooksEditor;

View File

@@ -1,165 +1,158 @@
import React from 'react'
import { styled } from '../stitches.config'
import * as LabelPrim from '@radix-ui/react-label'
import React from "react";
import { styled } from "../stitches.config";
export const Input = styled('input', {
export const Input = styled("input", {
// Reset
appearance: 'none',
borderWidth: '0',
boxSizing: 'border-box',
fontFamily: 'inherit',
outline: 'none',
width: '100%',
flex: '1',
backgroundColor: '$mauve4',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '$sm',
px: '$2',
fontSize: '$md',
appearance: "none",
borderWidth: "0",
boxSizing: "border-box",
fontFamily: "inherit",
outline: "none",
width: "100%",
flex: "1",
backgroundColor: "$mauve4",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "$sm",
px: "$2",
fontSize: "$md",
lineHeight: 1,
color: '$mauve12',
color: "$mauve12",
boxShadow: `0 0 0 1px $colors$mauve8`,
height: 35,
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
'&::before': {
boxSizing: 'border-box'
WebkitTapHighlightColor: "rgba(0,0,0,0)",
"&::before": {
boxSizing: "border-box",
},
'&::after': {
boxSizing: 'border-box'
"&::after": {
boxSizing: "border-box",
},
fontVariantNumeric: 'tabular-nums',
fontVariantNumeric: "tabular-nums",
'&:-webkit-autofill': {
boxShadow: 'inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3'
"&:-webkit-autofill": {
boxShadow: "inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3",
},
'&:-webkit-autofill::first-line': {
fontFamily: '$untitled',
color: '$mauve12'
"&:-webkit-autofill::first-line": {
fontFamily: "$untitled",
color: "$mauve12",
},
'&:focus': {
"&:focus": {
boxShadow: `0 0 0 1px $colors$mauve10`,
'&:-webkit-autofill': {
boxShadow: `0 0 0 1px $colors$mauve10`
}
"&:-webkit-autofill": {
boxShadow: `0 0 0 1px $colors$mauve10`,
},
},
'&::placeholder': {
color: '$mauve9'
"&::placeholder": {
color: "$mauve9",
},
'&:disabled': {
pointerEvents: 'none',
backgroundColor: '$mauve2',
color: '$mauve8',
cursor: 'not-allowed',
'&::placeholder': {
color: '$mauve7'
}
"&:disabled": {
pointerEvents: "none",
backgroundColor: "$mauve2",
color: "$mauve8",
cursor: "not-allowed",
"&::placeholder": {
color: "$mauve7",
},
},
'&:read-only': {
backgroundColor: '$mauve2',
color: '$text',
opacity: 0.8,
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve7'
}
"&:read-only": {
backgroundColor: "$mauve2",
"&:focus": {
boxShadow: "inset 0px 0px 0px 1px $colors$mauve7",
},
},
variants: {
size: {
sm: {
height: '$5',
fontSize: '$1',
lineHeight: '$sizes$4',
'&:-webkit-autofill::first-line': {
fontSize: '$1'
}
height: "$5",
fontSize: "$1",
lineHeight: "$sizes$4",
"&:-webkit-autofill::first-line": {
fontSize: "$1",
},
},
md: {
height: '$8',
fontSize: '$1',
lineHeight: '$sizes$5',
'&:-webkit-autofill::first-line': {
fontSize: '$1'
}
height: "$8",
fontSize: "$1",
lineHeight: "$sizes$5",
"&:-webkit-autofill::first-line": {
fontSize: "$1",
},
},
lg: {
height: '$12',
fontSize: '$2',
lineHeight: '$sizes$6',
'&:-webkit-autofill::first-line': {
fontSize: '$3'
}
}
height: "$12",
fontSize: "$2",
lineHeight: "$sizes$6",
"&:-webkit-autofill::first-line": {
fontSize: "$3",
},
},
},
variant: {
ghost: {
boxShadow: 'none',
backgroundColor: 'transparent',
'@hover': {
'&:hover': {
boxShadow: 'inset 0 0 0 1px $colors$mauve7'
}
boxShadow: "none",
backgroundColor: "transparent",
"@hover": {
"&:hover": {
boxShadow: "inset 0 0 0 1px $colors$mauve7",
},
},
'&:focus': {
backgroundColor: '$loContrast',
boxShadow: `0 0 0 1px $colors$mauve10`
"&:focus": {
backgroundColor: "$loContrast",
boxShadow: `0 0 0 1px $colors$mauve10`,
},
'&:disabled': {
backgroundColor: 'transparent'
"&:disabled": {
backgroundColor: "transparent",
},
"&:read-only": {
backgroundColor: "transparent",
},
'&:read-only': {
backgroundColor: 'transparent'
}
},
deep: {
backgroundColor: '$deep',
boxShadow: 'none'
}
backgroundColor: "$deep",
boxShadow: "none",
},
},
state: {
invalid: {
boxShadow: 'inset 0 0 0 1px $colors$crimson7',
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8'
}
boxShadow: "inset 0 0 0 1px $colors$crimson7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8",
},
},
valid: {
boxShadow: 'inset 0 0 0 1px $colors$grass7',
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8'
}
}
boxShadow: "inset 0 0 0 1px $colors$grass7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8",
},
},
},
cursor: {
default: {
cursor: 'default',
'&:focus': {
cursor: 'text'
}
cursor: "default",
"&:focus": {
cursor: "text",
},
},
text: {
cursor: 'text'
}
}
cursor: "text",
},
},
},
defaultVariants: {
size: 'md'
}
})
size: "md",
},
});
// eslint-disable-next-line react/display-name
const ReffedInput = React.forwardRef<HTMLInputElement, React.ComponentProps<typeof Input>>(
(props, ref) => <Input {...props} ref={ref} />
)
const ReffedInput = React.forwardRef<
HTMLInputElement,
React.ComponentProps<typeof Input>
>((props, ref) => <Input {...props} ref={ref} />);
export default ReffedInput
const LabelRoot = (props: LabelPrim.LabelProps) => <LabelPrim.Root {...props} />
export const Label = styled(LabelRoot, {
display: 'inline-block',
mb: '$1'
})
export default ReffedInput;

View File

@@ -1,8 +1,8 @@
import { styled } from '../stitches.config'
import { styled } from "../stitches.config";
const StyledLink = styled('a', {
color: 'CurrentColor',
textDecoration: 'underline',
const StyledLink = styled("a", {
color: "CurrentColor",
textDecoration: "underline",
cursor: 'pointer',
variants: {
highlighted: {
@@ -11,6 +11,6 @@ const StyledLink = styled('a', {
}
}
}
})
});
export default StyledLink
export default StyledLink;

View File

@@ -1,20 +1,24 @@
import { useRef, useLayoutEffect, ReactNode, FC, useState } from 'react'
import { IconProps, Notepad, Prohibit } from 'phosphor-react'
import useStayScrolled from 'react-stay-scrolled'
import NextLink from 'next/link'
import NextLink from "next/link";
import { Notepad, Prohibit } from "phosphor-react";
import {
FC, ReactNode, useCallback, useLayoutEffect, useRef, useState
} from "react";
import useStayScrolled from "react-stay-scrolled";
import regexifyString from "regexify-string";
import { useSnapshot } from "valtio";
import { Box, Button, Flex, Heading, Link, Pre, Text } from ".";
import state, { ILog } from "../state";
import { AccountDialog } from "./Accounts";
import Container from "./Container";
import LogText from "./LogText";
import Container from './Container'
import LogText from './LogText'
import { ILog } from '../state'
import { Pre, Link, Heading, Button, Text, Flex, Box } from '.'
interface ILogBox {
title: string
clearLog?: () => void
logs: ILog[]
renderNav?: () => ReactNode
enhanced?: boolean
Icon?: FC<IconProps>
title: string;
clearLog?: () => void;
logs: ILog[];
renderNav?: () => ReactNode;
enhanced?: boolean;
}
const LogBox: FC<ILogBox> = ({
@@ -24,40 +28,39 @@ const LogBox: FC<ILogBox> = ({
children,
renderNav,
enhanced,
Icon = Notepad
}) => {
const logRef = useRef<HTMLPreElement>(null)
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef)
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
useLayoutEffect(() => {
stayScrolled()
}, [stayScrolled, logs])
stayScrolled();
}, [stayScrolled, logs]);
return (
<Flex
as="div"
css={{
display: 'flex',
borderTop: '1px solid $mauve6',
background: '$mauve1',
position: 'relative',
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
flex: 1,
height: '100%'
height: "100%",
}}
>
<Container
css={{
px: 0,
height: '100%'
height: "100%",
}}
>
<Flex
fluid
css={{
height: '48px',
alignItems: 'center',
fontSize: '$sm',
fontWeight: 300
height: "48px",
alignItems: "center",
fontSize: "$sm",
fontWeight: 300,
}}
>
<Heading
@@ -65,27 +68,27 @@ const LogBox: FC<ILogBox> = ({
css={{
fontWeight: 300,
m: 0,
fontSize: '11px',
color: '$mauve12',
px: '$3',
textTransform: 'uppercase',
alignItems: 'center',
display: 'inline-flex',
gap: '$3'
fontSize: "11px",
color: "$mauve12",
px: "$3",
textTransform: "uppercase",
alignItems: "center",
display: "inline-flex",
gap: "$3",
}}
>
<Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
<Flex
row
align="center"
// css={{
// maxWidth: "100%", // TODO make it max without breaking layout!
// }}
css={{
width: "50%", // TODO make it max without breaking layout!
}}
>
{renderNav?.()}
</Flex>
<Flex css={{ ml: 'auto', gap: '$3', marginRight: '$3' }}>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
{clearLog && (
<Button ghost size="xs" onClick={clearLog}>
<Prohibit size="14px" />
@@ -100,17 +103,17 @@ const LogBox: FC<ILogBox> = ({
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'
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) => (
@@ -118,13 +121,13 @@ const LogBox: FC<ILogBox> = ({
as="span"
key={log.type + index}
css={{
'@hover': {
'&:hover': {
backgroundColor: enhanced ? '$backgroundAlt' : undefined
}
"@hover": {
"&:hover": {
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
},
},
p: enhanced ? '$1' : undefined,
my: enhanced ? '$1' : undefined
p: enhanced ? "$1" : undefined,
my: enhanced ? "$1" : undefined,
}}
>
<Log {...log} />
@@ -134,31 +137,67 @@ const LogBox: FC<ILogBox> = ({
</Box>
</Container>
</Flex>
)
}
);
};
export const Log: FC<ILog> = ({
type,
timestring,
message,
timestamp: timestamp,
message: _message,
link,
linkText,
defaultCollapsed,
jsonData
jsonData: _jsonData,
}) => {
const [expanded, setExpanded] = useState(!defaultCollapsed)
const [expanded, setExpanded] = useState(!defaultCollapsed);
const { accounts } = useSnapshot(state);
const [dialogAccount, setDialogAccount] = useState<string | null>(null);
const enrichAccounts = useCallback(
(str?: string): ReactNode => {
if (!str) 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]
);
_message = _message.trim().replace(/\n /gi, "\n");
const message = enrichAccounts(_message);
const jsonData = enrichAccounts(_jsonData);
if (message === undefined) message = <Text muted>{'undefined'}</Text>
else if (message === '') message = <Text muted>{'""'}</Text>
return (
<>
<AccountDialog
setActiveAccountAddress={setDialogAccount}
activeAccountAddress={dialogAccount}
/>
<LogText variant={type}>
{timestring && (
{timestamp && (
<Text muted monospace>
{timestring}{' '}
{timestamp}{" "}
</Text>
)}
<Pre>{message}</Pre>
<Pre>{message} </Pre>
{link && (
<NextLink href={link} shallow passHref>
<Link as="a">{linkText}</Link>
@@ -166,13 +205,14 @@ export const Log: FC<ILog> = ({
)}
{jsonData && (
<Link onClick={() => setExpanded(!expanded)} as="a">
{expanded ? 'Collapse' : 'Expand'}
{expanded ? "Collapse" : "Expand"}
</Link>
)}
{expanded && jsonData && <Pre block>{jsonData}</Pre>}
</LogText>
<br />
</>
)
}
);
};
export default LogBox
export default LogBox;

View File

@@ -1,31 +1,31 @@
import { styled } from '../stitches.config'
import { styled } from "../stitches.config";
const Text = styled('span', {
fontFamily: '$monospace',
lineHeight: '$body',
color: '$text',
wordWrap: 'break-word',
const Text = styled("span", {
fontFamily: "$monospace",
lineHeight: "$body",
color: "$text",
wordWrap: "break-word",
variants: {
variant: {
log: {
color: '$text'
color: "$text",
},
warning: {
color: '$warning'
color: "$warning",
},
error: {
color: '$error'
color: "$error",
},
success: {
color: '$success'
}
color: "$success",
},
},
capitalize: {
true: {
textTransform: 'capitalize'
}
}
}
})
textTransform: "capitalize",
},
},
},
});
export default Text
export default Text;

View File

@@ -1,15 +1,21 @@
import { styled } from '../stitches.config'
import { styled } from "../stitches.config";
const SVG = styled('svg', {
'& #path': {
fill: '$accent'
}
})
function Logo({ width, height }: { width?: string | number; height?: string | number }) {
const SVG = styled("svg", {
"& #path": {
fill: "$accent",
},
});
function Logo({
width,
height,
}: {
width?: string | number;
height?: string | number;
}) {
return (
<SVG
width={width || '1.1em'}
height={height || '1.1em'}
width={width || "1.1em"}
height={height || "1.1em"}
viewBox="0 0 294 283"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@@ -22,7 +28,7 @@ function Logo({ width, height }: { width?: string | number; height?: string | nu
fill="#9D2DFF"
/>
</SVG>
)
);
}
export default Logo
export default Logo;

View File

@@ -1,14 +0,0 @@
import ReactMarkdown from 'react-markdown'
import { styled } from '../stitches.config'
const Markdown = styled(ReactMarkdown, {
px: '$8',
'@md': {
px: '$20'
},
pb: '$5',
height: '100%',
overflowY: 'auto'
})
export default Markdown

View File

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

View File

@@ -1,90 +1,74 @@
import React from 'react'
import Link from 'next/link'
import React from "react";
import Link from "next/link";
import { useSnapshot } from 'valtio'
import { useRouter } from 'next/router'
import { FolderOpen, X, ArrowUpRight, BookOpen } from 'phosphor-react'
import { useSnapshot } from "valtio";
import { useRouter } from "next/router";
import { FolderOpen, X, ArrowUpRight, BookOpen } from "phosphor-react";
import Stack from './Stack'
import Logo from './Logo'
import Button from './Button'
import Flex from './Flex'
import Container from './Container'
import Box from './Box'
import ThemeChanger from './ThemeChanger'
import state from '../state'
import Heading from './Heading'
import Text from './Text'
import Spinner from './Spinner'
import truncate from '../utils/truncate'
import ButtonGroup from './ButtonGroup'
import Stack from "./Stack";
import Logo from "./Logo";
import Button from "./Button";
import Flex from "./Flex";
import Container from "./Container";
import Box from "./Box";
import ThemeChanger from "./ThemeChanger";
import state from "../state";
import Heading from "./Heading";
import Text from "./Text";
import Spinner from "./Spinner";
import truncate from "../utils/truncate";
import ButtonGroup from "./ButtonGroup";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger
} from './Dialog'
import PanelBox from './PanelBox'
import { templateFileIds } from '../state/constants'
import { styled } from '../stitches.config'
const ImageWrapper = styled(Flex, {
position: 'relative',
mt: '$2',
mb: '$10',
svg: {
// fill: "red",
'.angle': {
fill: '$text'
},
':not(.angle)': {
stroke: '$text'
}
}
})
DialogTrigger,
} from "./Dialog";
import PanelBox from "./PanelBox";
import { templateFileIds } from "../state/constants";
const Navigation = () => {
const router = useRouter()
const snap = useSnapshot(state)
const slug = router.query?.slug
const gistId = Array.isArray(slug) ? slug[0] : null
const router = useRouter();
const snap = useSnapshot(state);
const slug = router.query?.slug;
const gistId = Array.isArray(slug) ? slug[0] : null;
return (
<Box
as="nav"
css={{
display: 'flex',
backgroundColor: '$mauve1',
borderBottom: '1px solid $mauve6',
position: 'relative',
display: "flex",
backgroundColor: "$mauve1",
borderBottom: "1px solid $mauve6",
position: "relative",
zIndex: 2003,
height: '60px'
height: "60px",
}}
>
<Container
css={{
display: 'flex',
alignItems: 'center'
display: "flex",
alignItems: "center",
}}
>
<Flex
css={{
flex: 1,
alignItems: 'center',
borderRight: '1px solid $colors$mauve6',
py: '$3',
pr: '$4'
alignItems: "center",
borderRight: "1px solid $colors$mauve6",
py: "$3",
pr: "$4",
}}
>
<Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref>
<Link href={gistId ? `/develop/${gistId}` : "/develop"} passHref>
<Box
as="a"
css={{
display: 'flex',
alignItems: 'center',
color: '$textColor'
display: "flex",
alignItems: "center",
color: "$textColor",
}}
>
<Logo width="32px" height="32px" />
@@ -92,30 +76,38 @@ const Navigation = () => {
</Link>
<Flex
css={{
ml: '$5',
flexDirection: 'column',
gap: '1px'
ml: "$5",
flexDirection: "column",
gap: "1px",
}}
>
{snap.loading ? (
<Spinner />
) : (
<>
<Heading css={{ lineHeight: 1 }}>{snap.gistName || 'XRPL Hooks'}</Heading>
<Text css={{ fontSize: '$xs', color: '$mauve10', lineHeight: 1 }}>
{snap.files.length > 0 ? 'Gist: ' : 'Builder'}
<Heading css={{ lineHeight: 1 }}>
{snap.files?.[0]?.name || "XRPL Hooks"}
</Heading>
<Text
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
>
{snap.files.length > 0 ? "Gist: " : "Playground"}
{snap.files.length > 0 && (
<Link
href={`https://gist.github.com/${snap.gistOwner || ''}/${snap.gistId || ''}`}
href={`https://gist.github.com/${snap.gistOwner || ""}/${
snap.gistId || ""
}`}
passHref
>
<Text
as="a"
target="_blank"
rel="noreferrer noopener"
css={{ color: '$mauve12' }}
css={{ color: "$mauve12" }}
>
{`${snap.gistOwner || '-'}/${truncate(snap.gistId || '')}`}
{`${snap.gistOwner || "-"}/${truncate(
snap.gistId || ""
)}`}
</Text>
</Link>
)}
@@ -124,8 +116,11 @@ const Navigation = () => {
)}
</Flex>
{router.isReady && (
<ButtonGroup css={{ marginLeft: 'auto' }}>
<Dialog open={snap.mainModalOpen} onOpenChange={open => (state.mainModalOpen = open)}>
<ButtonGroup css={{ marginLeft: "auto" }}>
<Dialog
open={snap.mainModalOpen}
onOpenChange={(open) => (state.mainModalOpen = open)}
>
<DialogTrigger asChild>
<Button outline>
<FolderOpen size="15px" />
@@ -133,51 +128,50 @@ const Navigation = () => {
</DialogTrigger>
<DialogContent
css={{
display: 'flex',
maxWidth: '1080px',
width: '80vw',
maxHeight: '80%',
backgroundColor: '$mauve1 !important',
overflowY: 'auto',
background: 'black',
p: 0
maxWidth: "1080px",
width: "80vw",
height: "80%",
backgroundColor: "$mauve1 !important",
overflowY: "auto",
p: 0,
}}
>
<Flex
css={{
flexDirection: 'column',
height: '100%',
'@md': {
flexDirection: 'row',
height: '100%'
}
flexDirection: "column",
flex: 1,
height: "auto",
"@md": {
flexDirection: "row",
height: "100%",
},
}}
>
<Flex
css={{
borderBottom: '1px solid $colors$mauve5',
width: '100%',
minWidth: '240px',
flexDirection: 'column',
p: '$7',
backgroundColor: '$mauve2',
'@md': {
width: '30%',
maxWidth: '300px',
borderBottom: '0px',
borderRight: '1px solid $colors$mauve5'
}
borderBottom: "1px solid $colors$mauve5",
width: "100%",
flexDirection: "column",
p: "$7",
height: "100%",
backgroundColor: "$mauve2",
"@md": {
width: "30%",
maxWidth: "300px",
borderBottom: "0px",
borderRight: "1px solid $colors$mauve6",
},
}}
>
<DialogTitle
css={{
textTransform: 'uppercase',
display: 'inline-flex',
alignItems: 'center',
gap: '$3',
fontSize: '$xl',
lineHeight: '$one',
fontWeight: '$bold'
textTransform: "uppercase",
display: "inline-flex",
alignItems: "center",
gap: "$3",
fontSize: "$xl",
lineHeight: "$one",
fontWeight: "$bold",
}}
>
<Logo width="48px" height="48px" /> XRPL Hooks Builder
@@ -185,27 +179,30 @@ const Navigation = () => {
<DialogDescription as="div">
<Text
css={{
display: 'inline-flex',
color: 'inherit',
my: '$5',
mb: '$7'
display: "inline-flex",
color: "inherit",
my: "$5",
mb: "$7",
}}
>
Hooks add smart contract functionality to the XRP Ledger.
Hooks add smart contract functionality to the XRP
Ledger.
</Text>
<Flex css={{ flexDirection: 'column', gap: '$2', mt: '$2' }}>
<Flex
css={{ flexDirection: "column", gap: "$2", mt: "$2" }}
>
<Text
css={{
display: 'inline-flex',
alignItems: 'center',
gap: '$3',
color: '$purple11',
'&:hover': {
color: '$purple12'
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$purple10",
"&:hover": {
color: "$purple11",
},
"&:focus": {
outline: 0,
},
'&:focus': {
outline: 0
}
}}
as="a"
rel="noreferrer noopener"
@@ -217,16 +214,16 @@ const Navigation = () => {
<Text
css={{
display: 'inline-flex',
alignItems: 'center',
gap: '$3',
color: '$purple11',
'&:hover': {
color: '$purple12'
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$purple10",
"&:hover": {
color: "$purple11",
},
"&:focus": {
outline: 0,
},
'&:focus': {
outline: 0
}
}}
as="a"
rel="noreferrer noopener"
@@ -237,16 +234,16 @@ const Navigation = () => {
</Text>
<Text
css={{
display: 'inline-flex',
alignItems: 'center',
gap: '$3',
color: '$purple11',
'&:hover': {
color: '$purple12'
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$purple10",
"&:hover": {
color: "$purple11",
},
"&:focus": {
outline: 0,
},
'&:focus': {
outline: 0
}
}}
as="a"
rel="noreferrer noopener"
@@ -258,50 +255,79 @@ const Navigation = () => {
</Flex>
</DialogDescription>
</Flex>
<Flex
css={{
display: 'grid',
gridTemplateColumns: '1fr',
gridTemplateRows: 'max-content',
flex: 1,
p: '$7',
pb: '$16',
gap: '$3',
alignItems: 'normal',
flexWrap: 'wrap',
backgroundColor: '$mauve1',
'@md': {
gridTemplateColumns: '1fr 1fr',
gridTemplateRows: 'max-content'
},
'@lg': {
gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateRows: 'max-content'
}
}}
>
{Object.values(templateFileIds).map(template => (
<PanelBox key={template.id} as="a" href={`/develop/${template.id}`}>
<ImageWrapper>{template.icon()}</ImageWrapper>
<Heading>{template.name}</Heading>
<Text>{template.description}</Text>
<div>
<Flex
css={{
display: "grid",
gridTemplateColumns: "1fr",
gridTemplateRows: "max-content",
flex: 1,
p: "$7",
gap: "$3",
alignItems: "normal",
flexWrap: "wrap",
backgroundColor: "$mauve1",
"@md": {
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "max-content",
},
}}
>
<PanelBox
as="a"
href={`/develop/${templateFileIds.starter}`}
>
<Heading>Starter</Heading>
<Text>
Just a basic starter with essential imports
</Text>
</PanelBox>
))}
</Flex>
<PanelBox
as="a"
href={`/develop/${templateFileIds.firewall}`}
>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.notary}`}
>
<Heading>Notary</Heading>
<Text>
Collecting signatures for multi-sign transactions
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.carbon}`}
>
<Heading>Carbon</Heading>
<Text>Send a percentage of sum to an address</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.peggy}`}
>
<Heading>Peggy</Heading>
<Text>An oracle based stable coin hook</Text>
</PanelBox>
</Flex>
</div>
</Flex>
<DialogClose asChild>
<Box
css={{
position: 'absolute',
top: '$1',
right: '$1',
cursor: 'pointer',
background: '$mauve1',
display: 'flex',
borderRadius: '$full',
p: '$1'
position: "absolute",
top: "$1",
right: "$1",
cursor: "pointer",
background: "$mauve1",
display: "flex",
borderRadius: "$full",
p: "$1",
}}
>
<X size="20px" />
@@ -315,39 +341,61 @@ const Navigation = () => {
</Flex>
<Flex
css={{
flexWrap: 'nowrap',
marginLeft: '$4',
overflowX: 'scroll',
'&::-webkit-scrollbar': {
flexWrap: "nowrap",
marginLeft: "$4",
overflowX: "scroll",
"&::-webkit-scrollbar": {
height: 0,
background: 'transparent'
background: "transparent",
},
scrollbarColor: 'transparent',
scrollbarWidth: 'none'
}}
>
<Stack
css={{
ml: '$4',
gap: '$3',
flexWrap: 'nowrap',
alignItems: 'center',
marginLeft: 'auto'
ml: "$4",
gap: "$3",
flexWrap: "nowrap",
alignItems: "center",
marginLeft: "auto",
}}
>
<ButtonGroup>
<Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref shallow>
<Button as="a" outline={!router.pathname.includes('/develop')} uppercase>
<Link
href={gistId ? `/develop/${gistId}` : "/develop"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
>
Develop
</Button>
</Link>
<Link href={gistId ? `/deploy/${gistId}` : '/deploy'} passHref shallow>
<Button as="a" outline={!router.pathname.includes('/deploy')} uppercase>
<Link
href={gistId ? `/deploy/${gistId}` : "/deploy"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
>
Deploy
</Button>
</Link>
<Link href={gistId ? `/test/${gistId}` : '/test'} passHref shallow>
<Button as="a" outline={!router.pathname.includes('/test')} uppercase>
<Link
href={gistId ? `/test/${gistId}` : "/test"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
>
Test
</Button>
</Link>
@@ -363,7 +411,7 @@ const Navigation = () => {
</Flex>
</Container>
</Box>
)
}
);
};
export default Navigation
export default Navigation;

View File

@@ -1,30 +1,30 @@
import { styled } from '../stitches.config'
import Heading from './Heading'
import Text from './Text'
import { styled } from "../stitches.config";
import Heading from "./Heading";
import Text from "./Text";
const PanelBox = styled('div', {
display: 'flex',
flexDirection: 'column',
border: '1px solid $colors$mauve6',
backgroundColor: '$mauve2',
padding: '$3',
borderRadius: '$sm',
fontWeight: 'lighter',
height: 'auto',
cursor: 'pointer',
flex: '1 1 0px',
'&:hover': {
border: '1px solid $colors$mauve9'
const PanelBox = styled("div", {
display: "flex",
flexDirection: "column",
border: "1px solid $colors$mauve6",
backgroundColor: "$mauve2",
padding: "$3",
borderRadius: "$sm",
fontWeight: "lighter",
height: "auto",
cursor: "pointer",
flex: "1 1 0px",
"&:hover": {
border: "1px solid $colors$mauve9",
},
[`& ${Heading}`]: {
fontWeight: 'lighter',
mb: '$2'
fontWeight: "lighter",
mb: "$2",
},
[`& ${Text}`]: {
fontWeight: 'lighter',
color: '$mauve10',
fontSize: '$sm'
}
})
fontWeight: "lighter",
color: "$mauve10",
fontSize: "$sm",
},
});
export default PanelBox
export default PanelBox;

View File

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

View File

@@ -1,15 +1,15 @@
import { styled } from '../stitches.config'
import { styled } from "../stitches.config";
const Pre = styled('span', {
const Pre = styled("span", {
m: 0,
wordBreak: 'break-all',
wordBreak: "break-all",
fontFamily: '$monospace',
whiteSpace: 'pre-wrap',
variants: {
fluid: {
true: {
width: '100%'
}
width: "100%",
},
},
line: {
true: {
@@ -21,7 +21,7 @@ const Pre = styled('span', {
display: 'block'
}
}
}
})
},
});
export default Pre
export default Pre;

View File

@@ -1,24 +0,0 @@
import { FC } from 'react'
import { Link } from '.'
interface Props {
result?: string
}
const ResultLink: FC<Props> = ({ result }) => {
if (!result) return null
let href: string
if (result === 'tesSUCCESS') {
href = 'https://xrpl.org/tes-success.html'
} else {
// Going shortcut here because of url structure, if that changes we will do it manually
href = `https://xrpl.org/${result.slice(0, 3)}-codes.html`
}
return (
<Link as="a" href={href} target="_blank" rel="noopener noreferrer">
{result}
</Link>
)
}
export default ResultLink

View File

@@ -1,319 +0,0 @@
import { Play, X } from 'phosphor-react'
import { HTMLInputTypeAttribute, useCallback, useEffect, useState } from 'react'
import state, { IAccount, IFile, ILog } from '../../state'
import Button from '../Button'
import Box from '../Box'
import Input, { Label } from '../Input'
import Stack from '../Stack'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose
} from '../Dialog'
import Flex from '../Flex'
import { useSnapshot } from 'valtio'
import Select from '../Select'
import Text from '../Text'
import { saveFile } from '../../state/actions/saveFile'
import { getErrors, getTags } from '../../utils/comment-parser'
import toast from 'react-hot-toast'
const generateHtmlTemplate = async (code: string, data?: Record<string, any>) => {
let processString: string | undefined
const process = { env: { NODE_ENV: 'production' } } as any
if (data) {
Object.keys(data).forEach(key => {
process.env[key] = data[key]
})
}
processString = JSON.stringify(process)
const libs = (await import("xrpl-accountlib/dist/browser.hook-bundle.js")).default;
return `
<html>
<head>
<script>
var log = console.log;
var errorLog = console.error;
var infoLog = console.info;
var warnLog = console.warn
console.log = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'log', args: args || [] }, '*');
log.apply(console, args);
}
console.error = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'error', args: args || [] }, '*');
errorLog.apply(console, args);
}
console.info = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'info', args: args || [] }, '*');
infoLog.apply(console, args);
}
console.warn = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'warning', args: 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>
${libs}
</script>
<script type="module">
${code}
</script>
</head>
<body>
</body>
</html>
`
}
type Fields = Record<
string,
{
name: string
value: string
type?: 'Account' | `Account.${keyof IAccount}` | HTMLInputTypeAttribute
description?: string
required?: boolean
}
>
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
const snap = useSnapshot(state)
const [templateError, setTemplateError] = useState('')
const [fields, setFields] = useState<Fields>({})
const [iFrameCode, setIframeCode] = useState('')
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const getFields = useCallback(() => {
const inputTags = ['input', 'param', 'arg', 'argument']
const tags = getTags(content)
.filter(tag => inputTags.includes(tag.tag))
.filter(tag => !!tag.name)
let _fields = tags.map(tag => ({
name: tag.name,
value: tag.default || '',
type: tag.type,
description: tag.description,
required: !tag.optional
}))
const fields: Fields = _fields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {} as Fields)
const error = getErrors(content)
if (error) setTemplateError(error.message)
else setTemplateError('')
return fields
}, [content])
const runScript = useCallback(async () => {
setIsLoading(true);
try {
let data: any = {}
Object.keys(fields).forEach(key => {
data[key] = fields[key].value
})
const template = await generateHtmlTemplate(content, data)
setIframeCode(template)
state.scriptLogs = [{ type: 'success', message: 'Started running...' }]
} catch (err) {
state.scriptLogs = [
...snap.scriptLogs,
// @ts-expect-error
{ type: 'error', message: err?.message || 'Could not parse template' }
]
}
setIsLoading(false);
}, [content, fields, snap.scriptLogs])
useEffect(() => {
const handleEvent = (e: any) => {
if (e.data.type === 'log' || e.data.type === 'error') {
const data: ILog[] = e.data.args.map((msg: any) => ({
type: e.data.type,
message: typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)
}))
state.scriptLogs = [...snap.scriptLogs, ...data]
}
}
window.addEventListener('message', handleEvent)
return () => window.removeEventListener('message', handleEvent)
}, [snap.scriptLogs])
useEffect(() => {
const defaultFields = getFields() || {}
setFields(defaultFields)
}, [content, setFields, getFields])
const accOptions = snap.accounts?.map(acc => ({
...acc,
label: acc.name,
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 (
<>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
variant="primary"
onClick={() => {
saveFile(false)
setIframeCode('')
}}
>
<Play weight="bold" size="16px" /> {name}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Run {name} script</DialogTitle>
<DialogDescription>
<Box>
You are about to run scripts provided by the developer of the hook, make sure you
trust the author before you continue.
</Box>
{templateError && (
<Box
as="span"
css={{
display: 'block',
color: '$error',
mt: '$3',
whiteSpace: 'pre'
}}
>
{templateError}
</Box>
)}
{Object.keys(fields).length > 0 && (
<Box css={{ mt: '$4', mb: 0 }}>
Fill in the following parameters to run the script.
</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 css={{ justifyContent: 'flex-end', width: '100%', gap: '$3' }}>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" isDisabled={isDisabled || isLoading} isLoading={isLoading} onClick={handleRun}>
Run script
</Button>
</Flex>
</Stack>
<DialogClose asChild>
<Box
css={{
position: 'absolute',
top: '$1',
right: '$1',
cursor: 'pointer',
background: '$mauve1',
display: 'flex',
borderRadius: '$full',
p: '$1'
}}
>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
{iFrameCode && (
<iframe style={{ display: 'none' }} srcDoc={iFrameCode} sandbox="allow-scripts" />
)}
</>
)
}
export default RunScript

View File

@@ -1,15 +1,15 @@
import { forwardRef } from 'react'
import { mauve, mauveDark, purple, purpleDark } from '@radix-ui/colors'
import { useTheme } from 'next-themes'
import { styled } from '../stitches.config'
import dynamic from 'next/dynamic'
import type { Props } from 'react-select'
const SelectInput = dynamic(() => import('react-select'), { ssr: false })
import { forwardRef } from "react";
import { mauve, mauveDark, purple, purpleDark } from "@radix-ui/colors";
import { useTheme } from "next-themes";
import { styled } from "../stitches.config";
import dynamic from "next/dynamic";
import type { Props } from "react-select";
const SelectInput = dynamic(() => import("react-select"), { ssr: false });
// eslint-disable-next-line react/display-name
const Select = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const { theme } = useTheme();
const isDark = theme === "dark";
const colors: any = {
// primary: pink.pink9,
active: isDark ? purpleDark.purple9 : purple.purple9,
@@ -26,97 +26,127 @@ const Select = forwardRef<any, Props>((props, ref) => {
mauve9: isDark ? mauveDark.mauve9 : mauve.mauve9,
mauve12: isDark ? mauveDark.mauve12 : mauve.mauve12,
border: isDark ? mauveDark.mauve10 : mauve.mauve10,
placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11
}
colors.outline = colors.background
colors.selected = colors.secondary
placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11,
};
colors.outline = colors.background;
colors.selected = colors.secondary;
return (
<SelectInput
ref={ref}
menuPosition={props.menuPosition || 'fixed'}
menuPosition={props.menuPosition || "fixed"}
styles={{
container: provided => {
container: (provided) => {
return {
...provided,
position: 'relative'
}
position: "relative",
};
},
singleValue: provided => ({
singleValue: (provided) => ({
...provided,
color: colors.mauve12
color: colors.mauve12,
}),
menu: provided => ({
menu: (provided) => ({
...provided,
backgroundColor: colors.dropDownBg
backgroundColor: colors.dropDownBg,
}),
control: (provided, state) => {
return {
...provided,
minHeight: 0,
border: '0px',
border: "0px",
backgroundColor: colors.mauve4,
boxShadow: `0 0 0 1px ${state.isFocused ? colors.border : colors.secondary}`
}
boxShadow: `0 0 0 1px ${
state.isFocused ? colors.border : colors.secondary
}`,
};
},
input: provided => {
input: (provided) => {
return {
...provided,
color: '$text'
}
color: "$text",
};
},
multiValue: provided => {
multiValue: (provided) => {
return {
...provided,
backgroundColor: colors.mauve8
}
backgroundColor: colors.mauve8,
};
},
multiValueLabel: provided => {
multiValueLabel: (provided) => {
return {
...provided,
color: colors.mauve12
}
color: colors.mauve12,
};
},
multiValueRemove: provided => {
multiValueRemove: (provided) => {
return {
...provided,
':hover': {
background: colors.mauve9
}
}
":hover": {
background: colors.mauve9,
},
};
},
option: (provided, state) => {
return {
...provided,
color: colors.searchText,
backgroundColor: state.isFocused ? colors.activeLight : colors.dropDownBg,
':hover': {
backgroundColor:
state.isSelected || state.isFocused
? colors.activeLight
: colors.dropDownBg,
":hover": {
backgroundColor: colors.active,
color: '#ffffff'
color: "#ffffff",
},
':selected': {
backgroundColor: 'red'
}
}
":selected": {
backgroundColor: "red",
},
};
},
indicatorSeparator: provided => {
indicatorSeparator: (provided) => {
return {
...provided,
backgroundColor: colors.secondary
}
backgroundColor: colors.secondary,
};
},
dropdownIndicator: (provided, state) => {
return {
...provided,
color: state.isFocused ? colors.border : colors.secondary,
':hover': {
color: colors.border
}
}
}
":hover": {
color: colors.border,
},
};
},
}}
// theme={(theme) => ({
// ...theme,
// spacing: {
// ...theme.spacing,
// controlHeight: 30,
// },
// colors: {
// primary: colors.selected,
// primary25: colors.active,
// primary50: colors.primary,
// primary75: colors.primary,
// danger: colors.primary,
// dangerLight: colors.primary,
// neutral0: colors.background,
// neutral5: colors.primary,
// neutral10: colors.primary,
// neutral20: colors.outline,
// neutral30: colors.primary,
// neutral40: colors.primary,
// neutral50: colors.placeholder,
// neutral60: colors.primary,
// neutral70: colors.primary,
// neutral80: colors.searchText,
// neutral90: colors.primary,
// },
// })}
{...props}
/>
)
})
);
});
export default styled(Select, {})
export default styled(Select, {});

View File

@@ -1,342 +1,178 @@
import React, { useCallback, useEffect, useState } from 'react'
import { Plus, Trash, X } from 'phosphor-react'
import { Button, Box, Text } from '.'
import { Stack, Flex, Select } from '.'
import React, { useState } from "react";
import { Plus, Trash, X } from "phosphor-react";
import Button from "./Button";
import Box from "./Box";
import { Stack, Flex, Select } from ".";
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
DialogTrigger
} from './Dialog'
import { Input, Label } from './Input'
import { Controller, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'
DialogTrigger,
} from "./Dialog";
import { Input } from "./Input";
import {
Controller,
SubmitHandler,
useFieldArray,
useForm,
} from "react-hook-form";
import { deployHook } from '../state/actions'
import { useSnapshot } from 'valtio'
import state, { IFile, SelectOption } from '../state'
import toast from 'react-hot-toast'
import { prepareDeployHookTx, sha256 } from '../state/actions/deployHook'
import estimateFee from '../utils/estimateFee'
import { getParameters, getInvokeOptions, transactionOptions, SetHookData } from '../utils/setHook'
import { capitalize } from '../utils/helpers'
import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions";
import type { IAccount } from "../state";
import { useSnapshot } from "valtio";
import state from "../state";
import toast from "react-hot-toast";
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => {
const snap = useSnapshot(state)
const transactionOptions = Object.keys(tts).map((key) => ({
label: key,
value: key as keyof TTS,
}));
const [estimateLoading, setEstimateLoading] = useState(false)
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false)
export type SetHookData = {
Invoke: {
value: keyof TTS;
label: string;
}[];
HookParameters: {
HookParameter: {
HookParameterName: string;
HookParameterValue: string;
};
}[];
// HookGrants: {
// HookGrant: {
// Authorize: string;
// HookHash: string;
// };
// }[];
};
const compiledFiles = snap.files.filter(file => file.compiledContent)
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined
export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
const snap = useSnapshot(state);
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const {
register,
handleSubmit,
control,
// formState: { errors },
} = useForm<SetHookData>();
const { fields, append, remove } = useFieldArray({
control,
name: "HookParameters", // unique name for your Field Array
});
// const {
// fields: grantFields,
// append: grantAppend,
// remove: grantRemove,
// } = useFieldArray({
// control,
// name: "HookGrants", // unique name for your Field Array
// });
if (!account) {
return null;
}
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address
}))
const onSubmit: SubmitHandler<SetHookData> = async (data) => {
const currAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currAccount) currAccount.isLoading = true;
const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false;
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]?.HookNamespace) ||
activeFile?.name.split('.')[0] ||
'',
[activeFile, snap.deployValues]
)
const getDefaultValues = useCallback((): Partial<SetHookData> => {
const content = activeFile?.compiledValueSnapshot
return (
(activeFile && snap.deployValues[activeFile.name]) || {
HookNamespace: getHookNamespace(),
Invoke: getInvokeOptions(content),
HookParameters: getParameters(content)
}
)
}, [activeFile, getHookNamespace, snap.deployValues])
const {
register,
handleSubmit,
control,
watch,
setValue,
getValues,
reset,
formState: { errors }
} = useForm<SetHookData>({
defaultValues: getDefaultValues()
})
const { fields, append, remove } = useFieldArray({
control,
name: 'HookParameters' // unique name for your Field Array
})
const watchedFee = watch('Fee')
// Reset form if activeFile changes
useEffect(() => {
if (!activeFile) return
const defaultValues = getDefaultValues()
reset(defaultValues)
}, [activeFile, getDefaultValues, reset])
useEffect(() => {
if (watchedFee && (watchedFee.includes('.') || watchedFee.includes(','))) {
setValue('Fee', watchedFee.replaceAll('.', '').replaceAll(',', ''))
}
}, [watchedFee, setValue])
// const {
// fields: grantFields,
// append: grantAppend,
// remove: grantRemove,
// } = useFieldArray({
// control,
// name: "HookGrants", // unique name for your Field Array
// });
const [hashedNamespace, setHashedNamespace] = useState('')
const namespace = watch('HookNamespace', getHookNamespace())
const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace)
setHashedNamespace(hashedVal.toUpperCase())
}, [namespace])
useEffect(() => {
calculateHashedValue()
}, [namespace, calculateHashedValue])
const calculateFee = useCallback(async () => {
if (!account) return
const formValues = getValues()
const tx = await prepareDeployHookTx(account, formValues)
if (!tx) {
return
}
const res = await estimateFee(tx, account)
if (res && res.base_fee) {
setValue('Fee', Math.round(Number(res.base_fee || '')).toString())
}
}, [account, getValues, setValue])
const tooLargeFile = () => {
return Boolean(
activeFile?.compiledContent?.byteLength && activeFile?.compiledContent?.byteLength >= 64000
)
if (res && res.engine_result === "tesSUCCESS") {
toast.success("Transaction succeeded!");
return setIsSetHookDialogOpen(false);
}
const onSubmit: SubmitHandler<SetHookData> = async data => {
const currAccount = state.accounts.find(acc => acc.address === account?.address)
if (!account) return
if (currAccount) currAccount.isLoading = true
data.HookParameters.forEach(param => {
delete param.$metaData
return param
})
const res = await deployHook(account, data)
if (currAccount) currAccount.isLoading = false
if (res && res.engine_result === 'tesSUCCESS') {
toast.success('Transaction succeeded!')
return setIsSetHookDialogOpen(false)
}
toast.error(`Transaction failed! (${res?.engine_result_message})`)
}
const onOpenChange = useCallback(
(open: boolean) => {
setIsSetHookDialogOpen(open)
if (open) calculateFee()
},
[calculateFee]
)
return (
<Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button
ghost
size="xs"
uppercase
variant={'secondary'}
disabled={!account || account.isLoading || !activeFile || tooLargeFile()}
>
Set Hook
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogTitle>Deploy configuration</DialogTitle>
<DialogDescription as="div">
<Stack css={{ width: '100%', flex: 1 }}>
<Box css={{ width: '100%' }}>
<Label>Account</Label>
<Select
instanceId="deploy-account"
placeholder="Select account"
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)}
/>
</Box>
<Box css={{ width: '100%' }}>
<Label>Invoke on transactions</Label>
<Controller
name="Invoke"
control={control}
render={({ field }) => (
<Select
{...field}
closeMenuOnSelect={false}
isMulti
menuPosition="fixed"
options={transactionOptions}
/>
)}
/>
</Box>
<Box css={{ width: '100%' }}>
<Label>Hook Namespace Seed</Label>
<Input {...register('HookNamespace', { required: true })} autoComplete={'off'} />
{errors.HookNamespace?.type === 'required' && (
<Box css={{ display: 'inline', color: '$red11' }}>Namespace is required</Box>
toast.error(`Transaction failed! (${res?.engine_result_message})`);
};
return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<DialogTrigger asChild>
<Button
ghost
size="xs"
uppercase
variant={"secondary"}
disabled={
account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length
}
>
Set Hook
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogTitle>Deploy configuration</DialogTitle>
<DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}>
<Box css={{ width: "100%" }}>
<label>Invoke on transactions</label>
<Controller
name="Invoke"
control={control}
defaultValue={transactionOptions.filter(
(to) => to.label === "ttPAYMENT"
)}
<Box css={{ mt: '$3' }}>
<Label>Hook Namespace (sha256)</Label>
<Input readOnly value={hashedNamespace} />
</Box>
</Box>
<Box css={{ width: '100%' }}>
<Label style={{ marginBottom: '10px', display: 'block' }}>Hook parameters</Label>
<Stack>
{fields.map((field, index) => (
<Stack key={field.id}>
<Flex column>
<Flex row>
<Input
// important to include key with field's id
placeholder="Parameter name"
readOnly={field.$metaData?.required}
{...register(
`HookParameters.${index}.HookParameter.HookParameterName`
)}
/>
<Input
css={{ mx: '$2' }}
placeholder="Value (hex-quoted)"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`,
{ required: field.$metaData?.required }
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
</Flex>
{errors.HookParameters?.[index]?.HookParameter?.HookParameterValue
?.type === 'required' && <Text error>This field is required</Text>}
<Label css={{ fontSize: '$sm', mt: '$1' }}>
{capitalize(field.$metaData?.description)}
</Label>
</Flex>
</Stack>
))}
<Button
outline
fullWidth
type="button"
onClick={() =>
append({
HookParameter: {
HookParameterName: '',
HookParameterValue: ''
}
})
}
>
<Plus size="16px" />
Add Hook Parameter
</Button>
</Stack>
</Box>
<Box css={{ width: '100%', position: 'relative' }}>
<Label>Fee</Label>
<Box css={{ display: 'flex', alignItems: 'center' }}>
<Input
type="number"
{...register('Fee', { required: true })}
autoComplete={'off'}
onKeyPress={e => {
if (e.key === '.' || e.key === ',') {
e.preventDefault()
}
}}
step="1"
defaultValue={10000}
css={{
'-moz-appearance': 'textfield',
'&::-webkit-outer-spin-button': {
'-webkit-appearance': 'none',
margin: 0
},
'&::-webkit-inner-spin-button ': {
'-webkit-appearance': 'none',
margin: 0
}
}}
render={({ field }) => (
<Select
{...field}
closeMenuOnSelect={false}
isMulti
menuPosition="fixed"
options={transactionOptions}
/>
<Button
size="xs"
variant="primary"
outline
isLoading={estimateLoading}
css={{
position: 'absolute',
right: '$2',
fontSize: '$xs',
cursor: 'pointer',
alignContent: 'center',
display: 'flex'
}}
onClick={async e => {
e.preventDefault()
if (!account) return
setEstimateLoading(true)
const formValues = getValues()
try {
const tx = await prepareDeployHookTx(account, formValues)
if (tx) {
const res = await estimateFee(tx, account)
if (res && res.base_fee) {
setValue('Fee', Math.round(Number(res.base_fee || '')).toString())
}
}
} catch (err) {}
setEstimateLoading(false)
}}
>
Suggest
</Button>
</Box>
{errors.Fee?.type === 'required' && (
<Box css={{ display: 'inline', color: '$red11' }}>Fee is required</Box>
)}
</Box>
{/* <Box css={{ width: "100%" }}>
/>
</Box>
<Box css={{ width: "100%" }}>
<label style={{ marginBottom: "10px", display: "block" }}>
Hook parameters
</label>
<Stack>
{fields.map((field, index) => (
<Stack key={field.id}>
<Input
// important to include key with field's id
placeholder="Parameter name"
{...register(
`HookParameters.${index}.HookParameter.HookParameterName`
)}
/>
<Input
placeholder="Parameter value"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
</Stack>
))}
<Button
outline
fullWidth
type="button"
onClick={() =>
append({
HookParameter: {
HookParameterName: "",
HookParameterValue: "",
},
})
}
>
<Plus size="16px" />
Add Hook Parameter
</Button>
</Stack>
</Box>
{/* <Box css={{ width: "100%" }}>
<label style={{ marginBottom: "10px", display: "block" }}>
Hook Grants
</label>
@@ -384,37 +220,38 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
</Button>
</Stack>
</Box> */}
</Stack>
</DialogDescription>
</Stack>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: 'flex-end',
gap: '$3'
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
{/* <DialogClose asChild> */}
<Button variant="primary" type="submit" isLoading={account?.isLoading}>
Set Hook
</Button>
{/* </DialogClose> */}
</Flex>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
<X size="20px" />
</Box>
<Button outline>Cancel</Button>
</DialogClose>
</form>
</DialogContent>
</Dialog>
)
}
)
{/* <DialogClose asChild> */}
<Button
variant="primary"
type="submit"
isLoading={account.isLoading}
>
Set Hook
</Button>
{/* </DialogClose> */}
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</form>
</DialogContent>
</Dialog>
);
};
SetHookDialog.displayName = 'SetHookDialog'
export default SetHookDialog
export default SetHookDialog;

View File

@@ -1,14 +1,14 @@
import { Spinner as SpinnerIcon } from 'phosphor-react'
import { styled, keyframes } from '../stitches.config'
import { Spinner as SpinnerIcon } from "phosphor-react";
import { styled, keyframes } from "../stitches.config";
const rotate = keyframes({
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(-360deg)' }
})
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(-360deg)" },
});
const Spinner = styled(SpinnerIcon, {
animation: `${rotate} 150ms linear infinite`,
fontSize: '16px'
})
fontSize: "16px",
});
export default Spinner
export default Spinner;

View File

@@ -1,11 +1,11 @@
import Box from './Box'
import { styled } from '../stitches.config'
import Box from "./Box";
import { styled } from "../stitches.config";
const StackComponent = styled(Box, {
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
gap: '$4'
})
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
gap: "$4",
});
export default StackComponent
export default StackComponent;

View File

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

View File

@@ -1,58 +1,50 @@
import React, { useEffect, useState, Fragment, isValidElement, useCallback } from 'react'
import type { ReactNode, ReactElement } from 'react'
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from '.'
import React, {
useEffect,
useState,
Fragment,
isValidElement,
useCallback,
} from "react";
import type { ReactNode, ReactElement } from "react";
import { Box, Button, Flex, Input, Stack, Text } from ".";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose
} from './Dialog'
import { Plus, X } from 'phosphor-react'
import { styled } from '../stitches.config'
import { capitalize, getFileExtention } from '../utils/helpers'
import ContextMenu, { ContentMenuOption } from './ContextMenu'
DialogClose,
} from "./Dialog";
import { Plus, X } from "phosphor-react";
import { styled } from "../stitches.config";
const ErrorText = styled(Text, {
color: '$error',
mt: '$1',
display: 'block'
})
type Nullable<T> = T | null | undefined | false
color: "$error",
mt: "$1",
display: "block",
});
interface TabProps {
header: string
children?: ReactNode
renameDisabled?: boolean
header?: string;
children: ReactNode;
}
// TODO customize messages shown
// TODO customise strings shown
interface Props {
label?: string
activeIndex?: number
activeHeader?: string
headless?: boolean
children: ReactElement<TabProps>[]
keepAllAlive?: boolean
defaultExtension?: string
extensionRequired?: boolean
allowedExtensions?: string[]
headerExtraValidation?: {
regex: string | RegExp
error: string
}
onCreateNewTab?: (name: string) => any
onRenameTab?: (index: number, nwName: string, oldName?: string) => any
onCloseTab?: (index: number, header?: string) => any
onChangeActive?: (index: number, header?: string) => any
activeIndex?: number;
activeHeader?: string;
headless?: boolean;
children: ReactElement<TabProps>[];
keepAllAlive?: boolean;
defaultExtension?: string;
forceDefaultExtension?: boolean;
onCreateNewTab?: (name: string) => any;
onCloseTab?: (index: number, header?: string) => any;
}
export const Tab = (props: TabProps) => null
export const Tab = (props: TabProps) => null;
export const Tabs = ({
label = 'Tab',
children,
activeIndex,
activeHeader,
@@ -60,230 +52,164 @@ export const Tabs = ({
keepAllAlive = false,
onCreateNewTab,
onCloseTab,
onChangeActive,
onRenameTab,
headerExtraValidation,
extensionRequired,
defaultExtension = '',
allowedExtensions
defaultExtension = "",
forceDefaultExtension,
}: Props) => {
const [active, setActive] = useState(activeIndex || 0)
const tabs: TabProps[] = children.map(elem => elem.props)
const [active, setActive] = useState(activeIndex || 0);
const tabs: TabProps[] = children.map((elem) => elem.props);
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false)
const [renamingTab, setRenamingTab] = useState<number | null>(null)
const [tabname, setTabname] = useState('')
const [tabnameError, setTabnameError] = useState<string | null>(null)
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
const [tabname, setTabname] = useState("");
const [newtabError, setNewtabError] = useState<string | null>(null);
useEffect(() => {
if (activeIndex) setActive(activeIndex)
}, [activeIndex])
if (activeIndex) setActive(activeIndex);
}, [activeIndex]);
useEffect(() => {
if (activeHeader) {
const idx = tabs.findIndex(tab => tab.header === activeHeader)
if (idx !== -1) setActive(idx)
else setActive(0)
const idx = tabs.findIndex((tab) => tab.header === activeHeader);
setActive(idx);
}
}, [activeHeader, tabs])
}, [activeHeader, tabs]);
// when filename changes, reset error
useEffect(() => {
setTabnameError(null)
}, [tabname, setTabnameError])
setNewtabError(null);
}, [tabname, setNewtabError]);
const validateTabname = useCallback(
(tabname: string): { error?: string; result?: string } => {
if (!tabname) {
return { error: `Please enter ${label.toLocaleLowerCase()} name.` }
(tabname: string): { error: string | null } => {
if (tabs.find((tab) => tab.header === tabname)) {
return { error: "Name already exists." };
}
let ext = getFileExtention(tabname)
if (!ext && defaultExtension) {
ext = defaultExtension
tabname = `${tabname}.${defaultExtension}`
}
if (tabs.find(tab => tab.header === tabname)) {
return { error: `${capitalize(label)} name already exists.` }
}
if (extensionRequired && !ext) {
return { error: 'File extension is required!' }
}
if (allowedExtensions && ext && !allowedExtensions.includes(ext)) {
return { error: 'This file extension is not allowed!' }
}
if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) {
return { error: headerExtraValidation.error }
}
return { result: tabname }
return { error: null };
},
[allowedExtensions, defaultExtension, extensionRequired, headerExtraValidation, label, tabs]
)
const handleActiveChange = useCallback(
(idx: number, header?: string) => {
setActive(idx)
onChangeActive?.(idx, header)
},
[onChangeActive]
)
const handleRenameTab = useCallback(() => {
if (renamingTab === null) return
const res = validateTabname(tabname)
if (res.error) {
setTabnameError(`Error: ${res.error}`)
return
}
const { result: nwName = tabname } = res
setRenamingTab(null)
setTabname('')
const oldName = tabs[renamingTab]?.header
onRenameTab?.(renamingTab, nwName, oldName)
handleActiveChange(renamingTab, nwName)
}, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname])
[tabs]
);
const handleCreateTab = useCallback(() => {
const res = validateTabname(tabname)
if (res.error) {
setTabnameError(`Error: ${res.error}`)
return
// add default extension in case omitted
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = _tabname + defaultExtension;
}
const { result: _tabname = tabname } = res
setIsNewtabDialogOpen(false)
setTabname('')
const chk = validateTabname(_tabname);
if (chk.error) {
setNewtabError(`Error: ${chk.error}`);
return;
}
onCreateNewTab?.(_tabname)
setIsNewtabDialogOpen(false);
setTabname("");
// switch to new tab?
setActive(tabs.length);
handleActiveChange(tabs.length, _tabname)
}, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length])
onCreateNewTab?.(_tabname);
}, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
const handleCloseTab = useCallback(
(idx: number) => {
onCloseTab?.(idx, tabs[idx].header)
if (idx <= active && active !== 0) {
const nwActive = active - 1
handleActiveChange(nwActive, tabs[nwActive].header)
setActive(active - 1);
}
},
[active, handleActiveChange, onCloseTab, tabs]
)
const closeOption = (idx: number): Nullable<ContentMenuOption> =>
onCloseTab && {
type: 'text',
label: 'Close',
key: 'close',
onSelect: () => handleCloseTab(idx)
}
const renameOption = (idx: number, tab: TabProps): Nullable<ContentMenuOption> => {
return (
onRenameTab &&
!tab.renameDisabled && {
type: 'text',
label: 'Rename',
key: 'rename',
onSelect: () => setRenamingTab(idx)
}
)
}
onCloseTab?.(idx, tabs[idx].header);
},
[active, onCloseTab, tabs]
);
return (
<>
{!headless && (
<Stack
css={{
gap: '$3',
gap: "$3",
flex: 1,
flexWrap: 'nowrap',
marginBottom: '$2',
width: '100%',
overflow: 'auto'
flexWrap: "nowrap",
marginBottom: "-1px",
width: "100%",
overflow: "auto",
}}
>
{tabs.map((tab, idx) => (
<ContextMenu
<Button
key={tab.header}
options={
[closeOption(idx), renameOption(idx, tab)].filter(Boolean) as ContentMenuOption[]
}
role="tab"
tabIndex={idx}
onClick={() => setActive(idx)}
onKeyPress={() => setActive(idx)}
outline={active !== idx}
size="sm"
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
<Button
role="tab"
tabIndex={idx}
onClick={() => handleActiveChange(idx, tab.header)}
onKeyPress={() => handleActiveChange(idx, tab.header)}
outline={active !== idx}
size="sm"
css={{
'&:hover': {
span: {
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)
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
</ContextMenu>
{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);
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
))}
{onCreateNewTab && (
<Dialog open={isNewtabDialogOpen} onOpenChange={setIsNewtabDialogOpen}>
<Dialog
open={isNewtabDialogOpen}
onOpenChange={setIsNewtabDialogOpen}
>
<DialogTrigger asChild>
<Button ghost size="sm" css={{ alignItems: 'center', px: '$2', mr: '$3' }}>
<Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle>
<DialogTitle>Create new tab</DialogTitle>
<DialogDescription>
<Label>{label} name</Label>
<label>Tabname</label>
<Input
value={tabname}
onChange={e => setTabname(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter') {
handleCreateTab()
onChange={(e) => setTabname(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleCreateTab();
}
}}
/>
<ErrorText>{tabnameError}</ErrorText>
<ErrorText>{newtabError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: 'flex-end',
gap: '$3'
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
@@ -294,49 +220,7 @@ export const Tabs = ({
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
{onRenameTab && (
<Dialog open={renamingTab !== null} onOpenChange={() => setRenamingTab(null)}>
<DialogContent>
<DialogTitle>
Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre>
</DialogTitle>
<DialogDescription>
<Label>Enter new name</Label>
<Input
value={tabname}
onChange={e => setTabname(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter') {
handleRenameTab()
}
}}
/>
<ErrorText>{tabnameError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: 'flex-end',
gap: '$3'
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleRenameTab}>
Confirm
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
@@ -345,30 +229,29 @@ export const Tabs = ({
)}
</Stack>
)}
{keepAllAlive
? tabs.map((tab, idx) => {
// TODO Maybe rule out fragments as children
if (!isValidElement(tab.children)) {
if (active !== idx) return null
return tab.children
}
let key = tab.children.key || tab.header || idx
let { children } = tab
let { style, ...props } = children.props
return (
<children.type
key={key}
{...props}
style={{
...style,
display: active !== idx ? 'none' : undefined
}}
/>
)
})
: tabs[active] && (
<Fragment key={tabs[active].header || active}>{tabs[active].children}</Fragment>
)}
{keepAllAlive ? (
tabs.map((tab, idx) => {
// TODO Maybe rule out fragments as children
if (!isValidElement(tab.children)) {
if (active !== idx) return null;
return tab.children;
}
let key = tab.children.key || tab.header || idx;
let { children } = tab;
let { style, ...props } = children.props;
return (
<children.type
key={key}
{...props}
style={{ ...style, display: active !== idx ? "none" : undefined }}
/>
);
})
) : (
<Fragment key={tabs[active].header || active}>
{tabs[active].children}
</Fragment>
)}
</>
)
}
);
};

View File

@@ -1,9 +1,9 @@
import { styled } from '../stitches.config'
import { styled } from "../stitches.config";
const Text = styled('span', {
fontFamily: '$body',
lineHeight: '$body',
color: '$text',
const Text = styled("span", {
fontFamily: "$body",
lineHeight: "$body",
color: "$text",
variants: {
small: {
true: {
@@ -15,27 +15,12 @@ const Text = styled('span', {
color: '$mauve9'
}
},
error: {
true: {
color: '$error'
}
},
warning: {
true: {
color: '$warning'
}
},
monospace: {
true: {
fontFamily: '$monospace'
}
},
block: {
true: {
display: 'block'
}
}
}
})
});
export default Text
export default Text;

View File

@@ -1,113 +0,0 @@
import { styled } from '../stitches.config'
export const Textarea = styled('textarea', {
// Reset
appearance: 'none',
borderWidth: '0',
boxSizing: 'border-box',
fontFamily: 'inherit',
outline: 'none',
width: '100%',
flex: '1',
backgroundColor: '$mauve4',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '$sm',
p: '$2',
fontSize: '$md',
lineHeight: 1,
color: '$mauve12',
boxShadow: `0 0 0 1px $colors$mauve8`,
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
'&::before': {
boxSizing: 'border-box'
},
'&::after': {
boxSizing: 'border-box'
},
fontVariantNumeric: 'tabular-nums',
'&:-webkit-autofill': {
boxShadow: 'inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3'
},
'&:-webkit-autofill::first-line': {
fontFamily: '$untitled',
color: '$mauve12'
},
'&:focus': {
boxShadow: `0 0 0 1px $colors$mauve10`,
'&:-webkit-autofill': {
boxShadow: `0 0 0 1px $colors$mauve10`
}
},
'&::placeholder': {
color: '$mauve9'
},
'&:disabled': {
pointerEvents: 'none',
backgroundColor: '$mauve2',
color: '$mauve8',
cursor: 'not-allowed',
'&::placeholder': {
color: '$mauve7'
}
},
variants: {
variant: {
ghost: {
boxShadow: 'none',
backgroundColor: 'transparent',
'@hover': {
'&:hover': {
boxShadow: 'inset 0 0 0 1px $colors$mauve7'
}
},
'&:focus': {
backgroundColor: '$loContrast',
boxShadow: `0 0 0 1px $colors$mauve10`
},
'&:disabled': {
backgroundColor: 'transparent'
},
'&:read-only': {
backgroundColor: 'transparent'
}
},
deep: {
backgroundColor: '$deep',
boxShadow: 'none'
}
},
state: {
invalid: {
boxShadow: 'inset 0 0 0 1px $colors$crimson7',
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8'
}
},
valid: {
boxShadow: 'inset 0 0 0 1px $colors$grass7',
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8'
}
}
},
cursor: {
default: {
cursor: 'default',
'&:focus': {
cursor: 'text'
}
},
text: {
cursor: 'text'
}
}
}
})
export default Textarea

View File

@@ -1,34 +1,34 @@
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { Sun, Moon } from 'phosphor-react'
import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import { Sun, Moon } from "phosphor-react";
import Button from './Button'
import Button from "./Button";
const ThemeChanger = () => {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null
if (!mounted) return null;
return (
<Button
outline
onClick={() => {
setTheme(theme && theme === 'light' ? 'dark' : 'light')
setTheme(theme && theme === "light" ? "dark" : "light");
}}
css={{
display: 'flex',
marginLeft: 'auto',
cursor: 'pointer',
alignItems: 'center',
justifyContent: 'center',
color: '$muted'
display: "flex",
marginLeft: "auto",
cursor: "pointer",
alignItems: "center",
justifyContent: "center",
color: "$muted",
}}
>
{theme === 'dark' ? <Sun size="15px" /> : <Moon size="15px" />}
{theme === "dark" ? <Sun size="15px" /> : <Moon size="15px" />}
</Button>
)
}
);
};
export default ThemeChanger
export default ThemeChanger;

View File

@@ -1,90 +1,89 @@
import React from 'react'
import { styled, keyframes } from '../stitches.config'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import React from "react";
import { styled, keyframes } from "../stitches.config";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
const slideUpAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateY(2px)' },
'100%': { opacity: 1, transform: 'translateY(0)' }
})
"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)' }
})
"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)' }
})
"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)' }
})
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const StyledContent = styled(TooltipPrimitive.Content, {
borderRadius: 4,
padding: '$2 $3',
padding: "$2 $3",
fontSize: 12,
lineHeight: 1,
color: '$text',
backgroundColor: '$background',
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '400ms',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
animationFillMode: 'forwards',
willChange: 'transform, opacity',
color: "$text",
backgroundColor: "$background",
boxShadow:
"hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
"@media (prefers-reduced-motion: no-preference)": {
animationDuration: "400ms",
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
animationFillMode: "forwards",
willChange: "transform, opacity",
'&[data-state="delayed-open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade }
}
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
'.dark &': {
".dark &": {
boxShadow:
'0px 0px 10px 2px rgba(0,0,0,.45), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px'
"0px 0px 10px 2px rgba(255,255,255,.15), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
},
'.light &': {
".light &": {
boxShadow:
'0px 0px 10px 2px rgba(0,0,0,.25), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px'
}
})
"0px 0px 10px 2px rgba(0,0,0,.15), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
},
});
const StyledArrow = styled(TooltipPrimitive.Arrow, {
fill: '$background'
})
fill: "$background",
});
interface ITooltip {
content: string
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
content: string;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
}
const Tooltip: React.FC<React.ComponentProps<typeof StyledContent> & ITooltip> = ({
const Tooltip: React.FC<ITooltip> = ({
children,
content,
open,
defaultOpen = false,
onOpenChange,
...rest
}) => {
return (
<TooltipPrimitive.Root
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
delayDuration={100}
>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<StyledContent side="bottom" align="center" {...rest}>
<div dangerouslySetInnerHTML={{ __html: content }} />
<StyledContent side="bottom" align="center">
{content}
<StyledArrow offset={5} width={11} height={5} />
</StyledContent>
</TooltipPrimitive.Root>
)
}
);
};
export default Tooltip
export default Tooltip;

View File

@@ -1,238 +0,0 @@
import { Play } from 'phosphor-react'
import { FC, useCallback, useEffect } from 'react'
import { useSnapshot } from 'valtio'
import state from '../../state'
import {
defaultTransactionType,
getTxFields,
modifyTxState,
prepareState,
prepareTransaction,
SelectOption,
TransactionState
} from '../../state/transactions'
import { sendTransaction } from '../../state/actions'
import Box from '../Box'
import Button from '../Button'
import Flex from '../Flex'
import { TxJson } from './json'
import { TxUI } from './ui'
import { default as _estimateFee } from '../../utils/estimateFee'
import toast from 'react-hot-toast'
import { combineFlags, extractFlags, transactionFlags } from '../../state/constants/flags'
export interface TransactionProps {
header: string
state: TransactionState
}
const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props }) => {
const { accounts, editorSettings } = useSnapshot(state)
const { selectedAccount, selectedTransaction, txIsDisabled, txIsLoading, viewType, editorValue } =
txState
const setState = useCallback(
(pTx?: Partial<TransactionState>) => {
return modifyTxState(header, pTx)
},
[header]
)
const prepareOptions = useCallback(
(state: Partial<TransactionState> = txState) => {
const { selectedTransaction, selectedDestAccount, selectedAccount, txFields, selectedFlags } =
state
const TransactionType = selectedTransaction?.value || null
const Destination = selectedDestAccount?.value || txFields?.Destination
const Account = selectedAccount?.value || null
const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
return prepareTransaction({
...txFields,
Flags,
TransactionType,
Destination,
Account
})
},
[txState]
)
useEffect(() => {
const transactionType = selectedTransaction?.value
const account = selectedAccount?.value
if (!account || !transactionType || txIsLoading) {
setState({ txIsDisabled: true })
} else {
setState({ txIsDisabled: false })
}
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading])
const submitTest = useCallback(async () => {
let st: TransactionState | undefined
const tt = txState.selectedTransaction?.value
if (viewType === 'json') {
// save the editor state first
const pst = prepareState(editorValue || '', tt)
if (!pst) return
st = setState(pst)
}
const account = accounts.find(acc => acc.address === selectedAccount?.value)
if (txIsDisabled) return
setState({ txIsLoading: true })
const logPrefix = header ? `${header.split('.')[0]}: ` : undefined
try {
if (!account) {
throw Error('Account must be selected from imported accounts!')
}
const options = prepareOptions(st)
const fields = getTxFields(options.TransactionType)
if (fields.Destination && !options.Destination) {
throw Error('Destination account is required!')
}
await sendTransaction(account, options, { logPrefix })
} catch (error) {
console.error(error)
if (error instanceof Error) {
state.transactionLogs.push({
type: 'error',
message: `${logPrefix}${error.message}`
})
}
}
setState({ txIsLoading: false })
}, [
viewType,
accounts,
txIsDisabled,
setState,
header,
editorValue,
txState,
selectedAccount?.value,
prepareOptions
])
const getJsonString = useCallback(
(state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
[editorSettings.tabSize, prepareOptions]
)
const resetState = useCallback(
(transactionType: SelectOption | undefined = defaultTransactionType) => {
const fields = getTxFields(transactionType?.value)
const nwState: Partial<TransactionState> = {
viewType,
selectedTransaction: transactionType
}
if (fields.Destination !== undefined) {
nwState.selectedDestAccount = null
fields.Destination = ''
} else {
fields.Destination = undefined
}
if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) {
nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags)
fields.Flags = undefined
}
nwState.txFields = fields
const state = modifyTxState(header, nwState, { replaceState: true })
const editorValue = getJsonString(state)
return setState({ editorValue })
},
[getJsonString, header, setState, viewType]
)
const estimateFee = useCallback(
async (st?: TransactionState, opts?: { silent?: boolean }) => {
const state = st || txState
const ptx = prepareOptions(state)
const account = accounts.find(acc => acc.address === state.selectedAccount?.value)
if (!account) {
if (!opts?.silent) {
toast.error('Please select account from the list.')
}
return
}
ptx.Account = account.address
ptx.Sequence = account.sequence
const res = await _estimateFee(ptx, account, opts)
const fee = res?.base_fee
setState({ estimatedFee: fee })
return fee
},
[accounts, prepareOptions, setState, txState]
)
return (
<Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
{viewType === 'json' ? (
<TxJson
getJsonString={getJsonString}
header={header}
state={txState}
setState={setState}
estimateFee={estimateFee}
/>
) : (
<TxUI
state={txState}
resetState={resetState}
setState={setState}
estimateFee={estimateFee}
/>
)}
<Flex
row
css={{
justifyContent: 'space-between',
position: 'absolute',
left: 0,
bottom: 0,
width: '100%',
mb: '$1'
}}
>
<Button
onClick={() => {
if (viewType === 'ui') {
setState({ viewType: 'json' })
} else setState({ viewType: 'ui' })
}}
outline
>
{viewType === 'ui' ? 'EDIT AS JSON' : 'EXIT JSON MODE'}
</Button>
<Flex row>
<Button onClick={() => resetState()} outline css={{ mr: '$3' }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
)
}
export default Transaction

View File

@@ -1,209 +0,0 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useSnapshot } from 'valtio'
import state, { prepareState, transactionsData, TransactionState } from '../../state'
import Text from '../Text'
import { Flex, Link } from '..'
import { showAlert } from '../../state/actions/showAlert'
import { parseJSON } from '../../utils/json'
import { extractSchemaProps } from '../../utils/schema'
import amountSchema from '../../content/amount-schema.json'
import Monaco from '../Monaco'
import type monaco from 'monaco-editor'
interface JsonProps {
getJsonString?: (state?: Partial<TransactionState>) => string
header?: string
setState: (pTx?: Partial<TransactionState> | undefined) => void
state: TransactionState
estimateFee?: () => Promise<string | undefined>
}
export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, setState }) => {
const { editorSettings, accounts } = useSnapshot(state)
const { editorValue, estimatedFee } = txState
const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value
)
useEffect(() => {
setState({
editorValue: getJsonString?.()
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
const parsed = parseJSON(editorValue)
if (!parsed) return
const tt = parsed.TransactionType
const tx = transactionsData.find(t => t.TransactionType === tt)
if (tx) setCurrTxType(tx.TransactionType)
else {
setCurrTxType(undefined)
}
}, [editorValue])
const saveState = (value: string, transactionType?: string) => {
const tx = prepareState(value, transactionType)
if (tx) {
setState(tx)
setState({
editorValue: getJsonString?.(tx)
})
}
}
const discardChanges = () => {
showAlert('Confirm', {
body: 'Are you sure to discard these changes?',
confirmText: 'Yes',
onCancel: () => {},
onConfirm: () => setState({ editorValue: getJsonString?.() })
})
}
const onExit = (value: string) => {
const options = parseJSON(value)
if (options) {
saveState(value, currTxType)
return
}
showAlert('Error!', {
body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
confirmText: 'Discard',
onConfirm: () => setState({ editorValue: getJsonString?.() }),
onCancel: () => setState({ viewType: 'json' })
})
}
const getSchemas = useCallback(async (): Promise<any[]> => {
const txObj = transactionsData.find(td => td.TransactionType === currTxType)
let genericSchemaProps: any
if (txObj) {
genericSchemaProps = extractSchemaProps(txObj)
} else {
genericSchemaProps = transactionsData.reduce(
(cumm, td) => ({
...cumm,
...extractSchemaProps(td)
}),
{}
)
}
return [
{
uri: 'file:///main-schema.json', // id of the first schema
fileMatch: ['**.json'], // associate with our model
schema: {
title: header,
type: 'object',
required: ['TransactionType', 'Account'],
properties: {
...genericSchemaProps,
TransactionType: {
title: 'Transaction Type',
enum: transactionsData.map(td => td.TransactionType)
},
Account: {
$ref: 'file:///account-schema.json'
},
Destination: {
anyOf: [
{
$ref: 'file:///account-schema.json'
},
{
type: 'string',
title: 'Destination Account'
}
]
},
Amount: {
$ref: 'file:///amount-schema.json'
},
Fee: {
$ref: 'file:///fee-schema.json'
}
}
}
},
{
uri: 'file:///account-schema.json',
schema: {
type: 'string',
title: 'Account type',
enum: accounts.map(acc => acc.address)
}
},
{
uri: 'file:///fee-schema.json',
schema: {
type: 'string',
title: 'Fee type',
const: estimatedFee,
description: estimatedFee ? 'Above mentioned value is recommended base fee' : undefined
}
},
{
...amountSchema
}
]
}, [accounts, currTxType, estimatedFee, header])
const [monacoInst, setMonacoInst] = useState<typeof monaco>()
useEffect(() => {
if (!monacoInst) return
getSchemas().then(schemas => {
monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas
})
})
}, [getSchemas, monacoInst])
const hasUnsaved = useMemo(() => editorValue !== getJsonString?.(), [editorValue, getJsonString])
return (
<Monaco
rootProps={{
css: { height: 'calc(100% - 45px)' }
}}
language={'json'}
id={header}
height="100%"
value={editorValue}
onChange={val => setState({ editorValue: val })}
onMount={(editor, monaco) => {
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14
})
setMonacoInst(monaco)
// register onExit cb
const model = editor.getModel()
model?.onWillDispose(() => onExit(model.getValue()))
}}
overlay={
hasUnsaved ? (
<Flex row align="center" css={{ fontSize: '$xs', color: '$textMuted', ml: 'auto' }}>
<Text muted small>
This file has unsaved changes.
</Text>
<Link css={{ ml: '$1' }} onClick={() => saveState(editorValue || '', currTxType)}>
save
</Link>
<Link css={{ ml: '$1' }} onClick={discardChanges}>
discard
</Link>
</Flex>
) : undefined
}
/>
)
}

View File

@@ -1,343 +0,0 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import Container from '../Container'
import Flex from '../Flex'
import Input from '../Input'
import Select from '../Select'
import Text from '../Text'
import {
SelectOption,
TransactionState,
transactionsOptions,
TxFields,
getTxFields,
defaultTransactionType
} from '../../state/transactions'
import { useSnapshot } from 'valtio'
import state from '../../state'
import { streamState } from '../DebugStream'
import { Button } from '..'
import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags'
interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
resetState: (tt?: SelectOption) => TransactionState | undefined
state: TransactionState
estimateFee?: (...arg: any) => Promise<string | undefined>
}
export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estimateFee }) => {
const { accounts } = useSnapshot(state)
const { selectedAccount, selectedDestAccount, selectedTransaction, txFields, selectedFlags } =
txState
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address
}))
const destAccountOptions: SelectOption[] = accounts
.map(acc => ({
label: acc.name,
value: acc.address
}))
.filter(acc => acc.value !== selectedAccount?.value)
const flagsOptions: SelectOption[] = Object.entries(
getFlags(selectedTransaction?.value) || {}
).map(([label, value]) => ({
label,
value
}))
const [feeLoading, setFeeLoading] = useState(false)
const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc })
streamState.selectedAccount = acc
}
const handleSetField = useCallback(
(field: keyof TxFields, value: string, opFields?: TxFields) => {
const fields = opFields || txFields
const obj = fields[field]
setState({
txFields: {
...fields,
[field]: typeof obj === 'object' ? { ...obj, $value: value } : value
}
})
},
[setState, txFields]
)
const handleEstimateFee = useCallback(
async (state?: TransactionState, silent?: boolean) => {
setFeeLoading(true)
const fee = await estimateFee?.(state, { silent })
if (fee) handleSetField('Fee', fee, state?.txFields)
setFeeLoading(false)
},
[estimateFee, handleSetField]
)
const handleChangeTxType = useCallback(
(tt: SelectOption) => {
setState({ selectedTransaction: tt })
const newState = resetState(tt)
handleEstimateFee(newState, true)
},
[handleEstimateFee, resetState, setState]
)
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 richFields = ['TransactionType', 'Account']
if (fields.Destination !== undefined) {
richFields.push('Destination')
}
if (flagsOptions.length) {
richFields.push('Flags')
}
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
return (
<Container
css={{
p: '$3 01',
fontSize: '$sm',
height: 'calc(100% - 45px)'
}}
>
<Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
mt: '1px',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3' }}>
Transaction type:{' '}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: '70%' }}
value={selectedTransaction}
onChange={(tt: any) => handleChangeTxType(tt)}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3' }}>
Account:{' '}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: '70%' }}
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/>
</Flex>
{richFields.includes('Destination') && (
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3', textAlign: 'end' }}>
Destination account:{' '}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: '70%' }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/>
</Flex>
)}
{richFields.includes('Flags') && (
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3' }}>
Flags:{' '}
</Text>
<Select
isClearable
css={{ width: '70%' }}
instanceId="flags"
placeholder="Select flags to apply"
menuPosition="fixed"
value={selectedFlags}
isMulti
options={flagsOptions}
onChange={flags => setState({ selectedFlags: flags as any })}
closeMenuOnSelect={
selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false
}
/>
</Flex>
)}
{otherFields.map(field => {
let _value = txFields[field]
let value: string | undefined
if (typeof _value === 'object') {
if (_value.$type === 'json' && typeof _value.$value === 'object') {
value = JSON.stringify(_value.$value, null, 2)
} else {
value = _value.$value.toString()
}
} else {
value = _value?.toString()
}
const isXrp = typeof _value === 'object' && _value.$type === 'xrp'
const isJson = typeof _value === 'object' && _value.$type === 'json'
const isFee = field === 'Fee'
let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined
if (rows && rows > 5) rows = 5
return (
<Flex column key={field} css={{ mb: '$2', pr: '1px' }}>
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
position: 'relative'
}}
>
<Text muted css={{ mr: '$3' }}>
{field + (isXrp ? ' (XRP)' : '')}:{' '}
</Text>
{isJson ? (
<Textarea
rows={rows}
value={value}
spellCheck={false}
onChange={switchToJson}
css={{
width: '70%',
flex: 'inherit',
resize: 'vertical'
}}
/>
) : (
<Input
type={isFee ? 'number' : 'text'}
value={value}
onChange={e => {
if (isFee) {
const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
handleSetField(field, val)
} else {
handleSetField(field, e.target.value)
}
}}
onKeyPress={
isFee
? e => {
if (e.key === '.' || e.key === ',') {
e.preventDefault()
}
}
: undefined
}
css={{
width: '70%',
flex: 'inherit',
'-moz-appearance': 'textfield',
'&::-webkit-outer-spin-button': {
'-webkit-appearance': 'none',
margin: 0
},
'&::-webkit-inner-spin-button ': {
'-webkit-appearance': 'none',
margin: 0
}
}}
/>
)}
{isFee && (
<Button
size="xs"
variant="primary"
outline
disabled={txState.txIsDisabled}
isDisabled={txState.txIsDisabled}
isLoading={feeLoading}
css={{
position: 'absolute',
right: '$2',
fontSize: '$xs',
cursor: 'pointer',
alignContent: 'center',
display: 'flex'
}}
onClick={() => handleEstimateFee()}
>
Suggest
</Button>
)}
</Flex>
</Flex>
)
})}
</Flex>
</Container>
)
}

View File

@@ -1,34 +0,0 @@
const Carbon = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 24V30"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Carbon

View File

@@ -1,69 +0,0 @@
const Firewall = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M33 13V7"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M27 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M39 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 25V19"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21 13H45"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21 19H45"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M45 7H21V25H45V7Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Firewall

View File

@@ -1,34 +0,0 @@
const Notary = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M37.5 10.5L26.5 21.5L21 16.0002"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M49 10.5L38 21.5L35.0784 18.5785"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 5L8.94099 16.0625L4.00543e-05 27.125H2.27587L10.5015 16.9475H16.5938V15.1775H10.5015L2.27582 5H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 5L57.059 16.0625L66 27.125H63.7241L55.4985 16.9475H49.4062V15.1775H55.4985L63.7242 5H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Notary

View File

@@ -1,55 +0,0 @@
const Peggy = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M33 19C40.1797 19 46 16.3137 46 13C46 9.68629 40.1797 7 33 7C25.8203 7 20 9.68629 20 13C20 16.3137 25.8203 19 33 19Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 19V25"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M20 13V19C20 22 25 25 33 25C41 25 46 22 46 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M41 17.7633V23.7634"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M25 17.7633V23.7634"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Peggy

View File

@@ -1,34 +0,0 @@
const Starter = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M42 28H24C23.7347 28 23.4804 27.8946 23.2929 27.7071C23.1053 27.5196 23 27.2652 23 27V5C23 4.73479 23.1053 4.48044 23.2929 4.2929C23.4804 4.10537 23.7347 4.00001 24 4H36.0003L43 11V27C43 27.2652 42.8947 27.5196 42.7071 27.7071C42.5196 27.8946 42.2653 28 42 28V28Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M36 4V11H43.001"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Starter

View File

@@ -1,16 +1,17 @@
export { default as Flex } from './Flex'
export { default as Link } from './Link'
export { default as Container } from './Container'
export { default as Heading } from './Heading'
export { default as Stack } from './Stack'
export { default as Text } from './Text'
export { default as Input, Label } from './Input'
export { default as Select } from './Select'
export * from './Tabs'
export * from './AlertDialog/primitive'
export { default as Box } from './Box'
export { default as Button } from './Button'
export { default as Pre } from './Pre'
export { default as ButtonGroup } from './ButtonGroup'
export * from './Dialog'
export * from './DropdownMenu'
export { default as Flex } from "./Flex";
export { default as Link } from "./Link";
export { default as Container } from "./Container";
export { default as Heading } from "./Heading";
export { default as Stack } from "./Stack";
export { default as Text } from "./Text";
export { default as Input } from "./Input";
export { default as Select } from "./Select";
export * from "./Tabs";
export * from "./AlertDialog";
export { default as Box } from "./Box";
export { default as Button } from "./Button";
export { default as Pre } from "./Pre";
export { default as ButtonGroup } from "./ButtonGroup";
export { default as DeployFooter } from "./DeployFooter";
export * from "./Dialog";
export * from "./DropdownMenu";

View File

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

View File

@@ -1,409 +0,0 @@
[
{
"code": 1,
"identifier": "AMENDMENT_DISABLED",
"description": "attempt to HookSet when amendment is not yet enabled."
},
{
"code": 2,
"identifier": "API_ILLEGAL",
"description": "HookSet object contained HookApiVersion for existing HookDefinition"
},
{
"code": 3,
"identifier": "API_INVALID",
"description": "HookSet object contained HookApiVersion for unrecognised hook API "
},
{
"code": 4,
"identifier": "API_MISSING",
"description": "HookSet object lacked HookApiVersion"
},
{
"code": 5,
"identifier": "BLOCK_ILLEGAL",
"description": " a block end instruction moves execution below depth 0 {{}}`}` <= like this"
},
{
"code": 6,
"identifier": "CALL_ILLEGAL",
"description": "wasm tries to call a non-whitelisted function"
},
{
"code": 7,
"identifier": "CALL_INDIRECT",
"description": "wasm used call indirect instruction which is disallowed"
},
{
"code": 8,
"identifier": "CREATE_FLAG",
"description": "create operation requires hsoOVERRIDE"
},
{
"code": 9,
"identifier": "DELETE_FIELD",
"description": ""
},
{
"code": 10,
"identifier": "DELETE_FLAG",
"description": "delete operation requires hsoOVERRIDE"
},
{
"code": 11,
"identifier": "DELETE_NOTHING",
"description": "delete operation would delete nothing"
},
{
"code": 12,
"identifier": "EXPORTS_MISSING",
"description": "hook did not export *any* functions (should be cbak, hook)"
},
{
"code": 13,
"identifier": "EXPORT_CBAK_FUNC",
"description": "hook did not export correct func def int64_t cbak(uint32_t)"
},
{
"code": 14,
"identifier": "EXPORT_HOOK_FUNC",
"description": "hook did not export correct func def int64_t hook(uint32_t)"
},
{
"code": 15,
"identifier": "EXPORT_MISSING",
"description": "distinct from export*S*_missing, either hook or cbak is missing"
},
{
"identifier": "FLAGS_INVALID",
"code": 16,
"description": "HookSet flags were invalid for specified operation "
},
{
"identifier": "FUNCS_MISSING",
"code": 17,
"description": "hook did not include function code for any functions "
},
{
"identifier": "FUNC_PARAM_INVALID",
"code": 18,
"description": "parameter types may only be i32 i64 u32 u64 "
},
{
"identifier": "FUNC_RETURN_COUNT",
"code": 19,
"description": "a function type is defined in the wasm which returns > 1 return value "
},
{
"identifier": "FUNC_RETURN_INVALID",
"code": 20,
"description": "a function type does not return i32 i64 u32 or u64 "
},
{
"identifier": "FUNC_TYPELESS",
"code": 21,
"description": "hook defined hook/cbak but their type is not defined in wasm "
},
{
"identifier": "FUNC_TYPE_INVALID",
"code": 22,
"description": "malformed and illegal wasm in the func type section "
},
{
"identifier": "GRANTS_EMPTY",
"code": 23,
"description": "HookSet object contained an empty grants array (you should remove it) "
},
{
"identifier": "GRANTS_EXCESS",
"code": 24,
"description": "HookSet object cotnained a grants array with too many grants "
},
{
"identifier": "GRANTS_FIELD",
"code": 25,
"description": "HookSet object contained a grant without Authorize or HookHash "
},
{
"identifier": "GRANTS_ILLEGAL",
"code": 26,
"description": "Hookset object contained grants array which contained a non Grant object "
},
{
"identifier": "GUARD_IMPORT",
"code": 27,
"description": "guard import is missing "
},
{
"identifier": "GUARD_MISSING",
"code": 28,
"description": "guard call missing at top of loop "
},
{
"identifier": "GUARD_PARAMETERS",
"code": 29,
"description": "guard called but did not use constant expressions for params "
},
{
"identifier": "HASH_OR_CODE",
"code": 30,
"description": "HookSet object can contain only one of CreateCode and HookHash "
},
{
"identifier": "HOOKON_MISSING",
"code": 31,
"description": "HookSet object did not contain HookOn but should have "
},
{
"identifier": "HOOKS_ARRAY_BAD",
"code": 32,
"description": "attempt to HookSet with a Hooks array containing a non-Hook obj "
},
{
"identifier": "HOOKS_ARRAY_BLANK",
"code": 33,
"description": "all hook set objs were blank "
},
{
"identifier": "HOOKS_ARRAY_EMPTY",
"code": 34,
"description": "attempt to HookSet with an empty Hooks array "
},
{
"identifier": "HOOKS_ARRAY_MISSING",
"code": 35,
"description": "attempt to HookSet without a Hooks array "
},
{
"identifier": "HOOKS_ARRAY_TOO_BIG",
"code": 36,
"description": "attempt to HookSet with a Hooks array beyond the chain size limit "
},
{
"identifier": "HOOK_ADD",
"code": 37,
"description": "Informational: adding ltHook to directory "
},
{
"identifier": "HOOK_DEF_MISSING",
"code": 38,
"description": "attempt to reference a hook definition (by hash) that is not on ledger "
},
{
"identifier": "HOOK_DELETE",
"code": 39,
"description": "unable to delete ltHook from owner "
},
{
"identifier": "HOOK_INVALID_FIELD",
"code": 40,
"description": "HookSetObj contained an illegal/unexpected field "
},
{
"identifier": "HOOK_PARAMS_COUNT",
"code": 41,
"description": "hookset obj would create too many hook parameters "
},
{
"identifier": "HOOK_PARAM_SIZE",
"code": 42,
"description": "hookset obj sets a parameter or value that exceeds max allowable size "
},
{
"identifier": "IMPORTS_MISSING",
"code": 43,
"description": "hook must import guard, and accept/rollback "
},
{
"identifier": "IMPORT_ILLEGAL",
"code": 44,
"description": "attempted import of a non-whitelisted function "
},
{
"identifier": "IMPORT_MODULE_BAD",
"code": 45,
"description": "hook attempted to specify no or a bad import module "
},
{
"identifier": "IMPORT_MODULE_ENV",
"code": 46,
"description": "hook attempted to specify import module not named env "
},
{
"identifier": "IMPORT_NAME_BAD",
"code": 47,
"description": "import name was too short or too long "
},
{
"identifier": "INSTALL_FLAG",
"code": 48,
"description": "install operation requires hsoOVERRIDE "
},
{
"identifier": "INSTALL_MISSING",
"code": 49,
"description": "install operation specifies hookhash which doesn't exist on the ledger "
},
{
"identifier": "INSTRUCTION_COUNT",
"code": 50,
"description": "worst case execution instruction count as computed by HookSet "
},
{
"identifier": "INSTRUCTION_EXCESS",
"code": 51,
"description": "worst case execution instruction count was too large "
},
{
"identifier": "MEMORY_GROW",
"code": 52,
"description": "memory.grow instruction is present but disallowed "
},
{
"identifier": "NAMESPACE_MISSING",
"code": 53,
"description": "HookSet object lacked HookNamespace "
},
{
"identifier": "NSDELETE",
"code": 54,
"description": "Informational: a namespace is being deleted "
},
{
"identifier": "NSDELETE_ACCOUNT",
"code": 55,
"description": "nsdelete tried to delete ns from a non-existing account "
},
{
"identifier": "NSDELETE_COUNT",
"code": 56,
"description": "namespace state count less than 0 / overflow "
},
{
"identifier": "NSDELETE_DIR",
"code": 57,
"description": "could not delete directory node in ledger "
},
{
"identifier": "NSDELETE_DIRECTORY",
"code": 58,
"description": "nsdelete operation failed to delete ns directory "
},
{
"identifier": "NSDELETE_DIR_ENTRY",
"code": 59,
"description": "nsdelete operation failed due to bad entry in ns directory "
},
{
"identifier": "NSDELETE_ENTRY",
"code": 60,
"description": "nsdelete operation failed due to missing hook state entry "
},
{
"identifier": "NSDELETE_FIELD",
"code": 61
},
{
"identifier": "NSDELETE_FLAGS",
"code": 62
},
{
"identifier": "NSDELETE_NONSTATE",
"code": 63,
"description": "nsdelete operation failed due to the presence of a non-hookstate obj "
},
{
"identifier": "NSDELETE_NOTHING",
"code": 64,
"description": "hsfNSDELETE provided but nothing to delete "
},
{
"identifier": "OPERATION_INVALID",
"code": 65,
"description": "could not deduce an operation from the provided hookset obj "
},
{
"identifier": "OVERRIDE_MISSING",
"code": 66,
"description": "HookSet object was trying to update or delete a hook but lacked hsfOVERRIDE "
},
{
"identifier": "PARAMETERS_FIELD",
"code": 67,
"description": "HookParameters contained a HookParameter with an invalid key in it "
},
{
"identifier": "PARAMETERS_ILLEGAL",
"code": 68,
"description": "HookParameters contained something other than a HookParameter "
},
{
"identifier": "PARAMETERS_NAME",
"code": 69,
"description": "HookParameters contained a HookParameter which lacked ParameterName field "
},
{
"identifier": "PARAM_HOOK_CBAK",
"code": 70,
"description": "hook and cbak must take exactly one u32 parameter "
},
{
"identifier": "RETURN_HOOK_CBAK",
"code": 71,
"description": "hook and cbak must retunr i64 "
},
{
"identifier": "SHORT_HOOK",
"code": 72,
"description": "web assembly byte code ended abruptly "
},
{
"identifier": "TYPE_INVALID",
"code": 73,
"description": "malformed and illegal wasm specifying an illegal local var type "
},
{
"identifier": "WASM_BAD_MAGIC",
"code": 74,
"description": "wasm magic number missing or not wasm "
},
{
"identifier": "WASM_INVALID",
"code": 75,
"description": "set hook operation would set invalid wasm "
},
{
"identifier": "WASM_PARSE_LOOP",
"code": 76,
"description": "wasm section parsing resulted in an infinite loop "
},
{
"identifier": "WASM_SMOKE_TEST",
"code": 77,
"description": "Informational: first attempt to load wasm into wasm runtime "
},
{
"identifier": "WASM_TEST_FAILURE",
"code": 78,
"description": "the smoke test failed "
},
{
"identifier": "WASM_TOO_BIG",
"code": 79,
"description": "set hook would exceed maximum hook size "
},
{
"identifier": "WASM_TOO_SMALL",
"code": 80
},
{
"identifier": "WASM_VALIDATION",
"code": 81,
"description": "a generic error while parsing wasm, usually leb128 overflow"
},
{
"identifier": "HOOK_CBAK_DIFF_TYPES",
"code": 82,
"description": "hook and cbak function definitions were different"
}
]

View File

@@ -6,7 +6,7 @@
"DestinationTag": 13,
"Fee": "2000000",
"Sequence": 2470665,
"Flags": "2147483648"
"Flags": 2147483648
},
{
"TransactionType": "AccountSet",
@@ -27,8 +27,8 @@
"Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"TransactionType": "CheckCash",
"Amount": {
"$value": "100",
"$type": "xrp"
"value": "100",
"type": "currency"
},
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12"
@@ -48,30 +48,28 @@
"Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
"Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
"Fee": "10",
"Flags": "2147483648",
"Flags": 2147483648,
"Sequence": 2
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7,
"Fee": "10"
"OfferSequence": 7
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCreate",
"Amount": {
"$value": "100",
"$type": "xrp"
"value": "100",
"type": "currency"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958,
"FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"DestinationTag": 23480,
"SourceTag": 11747,
"Fee": "10"
"SourceTag": 11747
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -79,58 +77,38 @@
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000",
"Fee": "10"
},
{
"TransactionType": "NFTokenMint",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenTaxon": 0,
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
"Fulfillment": "A0028000"
},
{
"TransactionType": "NFTokenBurn",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
"TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
},
{
"TransactionType": "NFTokenAcceptOffer",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenSellOffer": "A2FA1A9911FE2AEF83DAB05F437768E26A301EF899BD31EB85E704B3D528FF18",
"NFTokenBuyOffer": "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862",
"NFTokenBrokerFee": "10"
"Fee": "10"
},
{
"TransactionType": "NFTokenCancelOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "10",
"NFTokenOffers": {
"$type": "json",
"$value": [
"4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"
]
}
"TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
},
{
"TransactionType": "NFTokenCreateOffer",
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": {
"$value": "100",
"$type": "xrp"
"value": "100",
"type": "currency"
},
"Flags": "1",
"Destination": "",
"Fee": "10"
"Flags": 1
},
{
"TransactionType": "OfferCancel",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": "0",
"Flags": 0,
"LastLedgerSequence": 7108629,
"OfferSequence": 6,
"Sequence": 7
@@ -139,13 +117,13 @@
"TransactionType": "OfferCreate",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": "0",
"Flags": 0,
"LastLedgerSequence": 7108682,
"Sequence": 8,
"TakerGets": "6000000",
"Amount": {
"$value": "100",
"$type": "xrp"
"value": "100",
"type": "currency"
}
},
{
@@ -153,55 +131,53 @@
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Amount": {
"$value": "100",
"$type": "xrp"
"value": "100",
"type": "currency"
},
"Fee": "12",
"Flags": "2147483648",
"Flags": 2147483648,
"Sequence": 2
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelCreate",
"Amount": {
"$value": "100",
"$type": "xrp"
"value": "100",
"type": "currency"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400,
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558,
"DestinationTag": 23480,
"SourceTag": 11747,
"Fee": "10"
"SourceTag": 11747
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelFund",
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": {
"$value": "200",
"$type": "xrp"
"value": "200",
"type": "currency"
},
"Expiration": 543171558,
"Fee": "10"
"Expiration": 543171558
},
{
"Flags": "0",
"Flags": 0,
"TransactionType": "SetRegularKey",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12",
"RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
},
{
"Flags": "0",
"Flags": 0,
"TransactionType": "SignerListSet",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12",
"SignerQuorum": 3,
"SignerEntries": {
"$type": "json",
"$value": [
"type": "json",
"value": [
{
"SignerEntry": {
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
@@ -234,25 +210,12 @@
"TransactionType": "TrustSet",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": "262144",
"Flags": 262144,
"LastLedgerSequence": 8007750,
"LimitAmount": {
"$type": "json",
"$value": {
"currency": "USD",
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
"value": "100"
}
"Amount": {
"value": "100",
"type": "currency"
},
"Sequence": 12
},
{
"TransactionType": "Invoke",
"Fee": "12"
},
{
"TransactionType": "UriToken",
"Fee": "12",
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
}
]

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react'
import { useEffect, useState } from "react";
// Define general type for useWindowSize hook, which includes width and height
interface Size {
width: number | undefined
height: number | undefined
width: number | undefined;
height: number | undefined;
}
// Hook
@@ -12,25 +12,25 @@ function useWindowSize(): Size {
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState<Size>({
width: undefined,
height: undefined
})
height: undefined,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
})
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener('resize', handleResize)
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize()
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener('resize', handleResize)
}, []) // Empty array ensures that effect is only run on mount
return windowSize
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
export default useWindowSize
export default useWindowSize;

View File

@@ -2,19 +2,19 @@
module.exports = {
reactStrictMode: true,
images: {
domains: ['avatars.githubusercontent.com']
domains: ["avatars.githubusercontent.com"],
},
webpack(config, { isServer }) {
config.resolve.alias['vscode'] = require.resolve(
'@codingame/monaco-languageclient/lib/vscode-compatibility'
)
config.resolve.alias["vscode"] = require.resolve(
"@codingame/monaco-languageclient/lib/vscode-compatibility"
);
if (!isServer) {
config.resolve.fallback.fs = false
config.resolve.fallback.fs = false;
}
config.module.rules.push({
test: [/\.md$/, /hook-bundle\.js$/],
use: 'raw-loader'
})
return config
}
}
test: /\.md$/,
use: "raw-loader",
});
return config;
},
};

View File

@@ -7,28 +7,21 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write .",
"postinstall": "patch-package && yarn run postinstall-postinstall",
"postinstall-postinstall": "./node_modules/.bin/browserify -r ripple-binary-codec -r ripple-keypairs -r ripple-address-codec -r ripple-secret-codec -r ./node_modules/xrpl-accountlib/dist/index.js:xrpl-accountlib -o node_modules/xrpl-accountlib/dist/browser.hook-bundle.js"
"postinstall": "patch-package"
},
"dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.4.5",
"@monaco-editor/react": "^4.3.1",
"@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7",
"@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-dropdown-menu": "^0.1.6",
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6",
"@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.8",
"@stitches/react": "^1.2.6-0",
"base64-js": "^1.5.1",
"comment-parser": "^1.3.1",
"dinero.js": "^1.9.1",
"file-saver": "^2.0.5",
"filesize": "^8.0.7",
@@ -36,25 +29,22 @@
"jszip": "^3.7.1",
"lodash.uniqby": "^4.7.0",
"lodash.xor": "^4.5.0",
"monaco-editor": "^0.33.0",
"monaco-editor": "^0.30.1",
"next": "^12.0.4",
"next-auth": "^4.10.3",
"next-plausible": "^3.2.0",
"next-themes": "^0.1.1",
"next-auth": "^4.0.0-beta.5",
"next-themes": "^0.0.15",
"normalize-url": "^7.0.2",
"octokit": "^1.7.0",
"pako": "^2.0.4",
"patch-package": "^6.4.7",
"phosphor-react": "^1.3.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.7.1",
"re-resizable": "^6.9.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-hook-form": "^7.28.0",
"react-hot-keys": "^2.7.1",
"react-hot-toast": "^2.1.1",
"react-markdown": "^8.0.3",
"react-new-window": "^0.2.1",
"react-select": "^5.2.1",
"react-split": "^2.0.14",
@@ -65,9 +55,9 @@
"valtio": "^1.2.5",
"vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2",
"wabt": "^1.0.30",
"xrpl-accountlib": "^1.6.1",
"xrpl-client": "^2.0.2"
"wabt": "1.0.16",
"xrpl-accountlib": "^1.3.2",
"xrpl-client": "^1.9.4"
},
"devDependencies": {
"@types/dinero.js": "^1.9.0",
@@ -76,13 +66,9 @@
"@types/lodash.xor": "^4.5.6",
"@types/pako": "^1.0.2",
"@types/react": "17.0.31",
"browserify": "^17.0.0",
"eslint": "7.32.0",
"eslint-config-next": "11.1.2",
"raw-loader": "^4.0.2",
"typescript": "4.4.4"
},
"resolutions": {
"ripple-binary-codec": "=1.4.2"
}
}

View File

@@ -1,54 +1,56 @@
import { useEffect } from 'react'
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import { SessionProvider } from 'next-auth/react'
import { ThemeProvider } from 'next-themes'
import { Toaster } from 'react-hot-toast'
import { useRouter } from 'next/router'
import { IdProvider } from '@radix-ui/react-id'
import PlausibleProvider from 'next-plausible'
import { useEffect } from "react";
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Head from "next/head";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";
import { Toaster } from "react-hot-toast";
import { useRouter } from "next/router";
import { IdProvider } from "@radix-ui/react-id";
import { darkTheme, css } from '../stitches.config'
import Navigation from '../components/Navigation'
import { fetchFiles } from '../state/actions'
import state from '../state'
import { darkTheme, css } from "../stitches.config";
import Navigation from "../components/Navigation";
import { fetchFiles } from "../state/actions";
import state from "../state";
import TimeAgo from 'javascript-time-ago'
import en from 'javascript-time-ago/locale/en.json'
import { useSnapshot } from 'valtio'
import Alert from '../components/AlertDialog'
import { Button, Flex } from '../components'
import { ChatCircleText } from 'phosphor-react'
import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en.json";
import { useSnapshot } from "valtio";
TimeAgo.setDefaultLocale(en.locale)
TimeAgo.setDefaultLocale(en.locale);
TimeAgo.addLocale(en)
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
const router = useRouter()
const slug = router.query?.slug
const gistId = (Array.isArray(slug) && slug[0]) ?? null
const router = useRouter();
const slug = router.query?.slug;
const gistId = (Array.isArray(slug) && slug[0]) ?? null;
const origin = 'https://xrpl-hooks-ide.vercel.app' // TODO: Change when site is deployed
const shareImg = '/share-image.png'
const origin = "https://xrpl-hooks-ide.vercel.app"; // TODO: Change when site is deployed
const shareImg = "/share-image.png";
const snap = useSnapshot(state)
const snap = useSnapshot(state);
useEffect(() => {
if (gistId && router.isReady) {
fetchFiles(gistId)
fetchFiles(gistId);
} else {
if (
!gistId &&
router.isReady &&
router.pathname.includes('/develop') &&
!router.pathname.includes("/sign-in") &&
!snap.files.length &&
!snap.mainModalShowed
) {
state.mainModalOpen = true
state.mainModalShowed = true
state.mainModalOpen = true;
state.mainModalShowed = true;
}
}
}, [gistId, router.isReady, router.pathname, snap.files, snap.mainModalShowed])
}, [
gistId,
router.isReady,
router.pathname,
snap.files,
snap.mainModalShowed,
]);
return (
<>
@@ -58,38 +60,59 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
<meta name="format-detection" content="telephone=no" />
<meta property="og:url" content={`${origin}${router.asPath}`} />
<title>XRPL Hooks Builder</title>
<meta property="og:title" content="XRPL Hooks Builder" />
<meta name="twitter:title" content="XRPL Hooks Builder" />
<title>XRPL Hooks Editor</title>
<meta property="og:title" content="XRPL Hooks Editor" />
<meta name="twitter:title" content="XRPL Hooks Editor" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@XRPLF" />
<meta name="twitter:site" content="@xrpllabs" />
<meta
name="description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
/>
<meta
property="og:description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
/>
<meta
name="twitter:description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger.."
/>
<meta property="og:image" content={`${origin}${shareImg}`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:image" content={`${origin}${shareImg}`} />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
<meta name="application-name" content="XRPL Hooks Builder" />
<meta name="application-name" content="XRPL Hooks Editor" />
<meta name="msapplication-TileColor" content="#c10ad0" />
<meta name="theme-color" content="#161618" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#FDFCFD" media="(prefers-color-scheme: light)" />
<meta
name="theme-color"
content="#161618"
media="(prefers-color-scheme: dark)"
/>
<meta
name="theme-color"
content="#FDFCFD"
media="(prefers-color-scheme: light)"
/>
</Head>
<IdProvider>
<SessionProvider session={session}>
<ThemeProvider
@@ -97,45 +120,30 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
defaultTheme="dark"
enableSystem={false}
value={{
light: 'light',
dark: darkTheme.className
light: "light",
dark: darkTheme.className,
}}
>
<PlausibleProvider domain="hooks-builder.xrpl.org" trackOutboundLinks>
<Navigation />
<Component {...pageProps} />
<Toaster
toastOptions={{
className: css({
backgroundColor: '$mauve1',
color: '$mauve10',
fontSize: '$sm',
zIndex: 9999,
'.dark &': {
backgroundColor: '$mauve4',
color: '$mauve12'
}
})()
}}
/>
<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>
<Navigation />
<Component {...pageProps} />
<Toaster
toastOptions={{
className: css({
backgroundColor: "$mauve1",
color: "$mauve10",
fontSize: "$sm",
zIndex: 9999,
".dark &": {
backgroundColor: "$mauve4",
color: "$mauve12",
},
})(),
}}
/>
</ThemeProvider>
</SessionProvider>
</IdProvider>
</>
)
);
}
export default MyApp
export default MyApp;

View File

@@ -1,22 +1,35 @@
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from "next/document";
import { globalStyles, getCssText } from '../stitches.config'
import { globalStyles, getCssText } from "../stitches.config";
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx)
const initialProps = await Document.getInitialProps(ctx);
return initialProps
return initialProps;
}
render() {
globalStyles()
globalStyles();
return (
<Html>
<Head>
<style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
<style
id="stitches"
dangerouslySetInnerHTML={{ __html: getCssText() }}
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap"
rel="stylesheet"
@@ -27,8 +40,8 @@ class MyDocument extends Document {
<NextScript />
</body>
</Html>
)
);
}
}
export default MyDocument
export default MyDocument;

View File

@@ -1,10 +1,12 @@
import type { NextRequest, NextFetchEvent } from 'next/server'
import { NextResponse as Response } from 'next/server'
import type { NextRequest, NextFetchEvent } from 'next/server';
import { NextResponse as Response } from 'next/server';
export default function middleware(req: NextRequest, ev: NextFetchEvent) {
if (req.nextUrl.pathname === '/') {
const url = req.nextUrl.clone()
url.pathname = '/develop'
return Response.redirect(url)
if (req.nextUrl.pathname === "/") {
const url = req.nextUrl.clone();
url.pathname = '/develop';
return Response.redirect(url);
}
}
}

View File

@@ -1,4 +1,4 @@
import NextAuth from 'next-auth'
import NextAuth from "next-auth"
export default NextAuth({
// Configure one or more authentication providers
@@ -10,38 +10,39 @@ export default NextAuth({
// scope: 'user,gist'
// }),
{
id: 'github',
name: 'GitHub',
type: 'oauth',
id: "github",
name: "GitHub",
type: "oauth",
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
authorization: 'https://github.com/login/oauth/authorize?scope=read:user+user:email+gist',
token: 'https://github.com/login/oauth/access_token',
userinfo: 'https://api.github.com/user',
authorization: "https://github.com/login/oauth/authorize?scope=read:user+user:email+gist",
token: "https://github.com/login/oauth/access_token",
userinfo: "https://api.github.com/user",
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name || profile.login,
username: profile.login,
email: profile.email,
image: profile.avatar_url
image: profile.avatar_url,
}
}
},
}
// ...add more providers here
],
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
if (account && account.access_token) {
token.accessToken = account.access_token
token.username = user?.username || ''
token.accessToken = account.access_token;
token.username = user?.username || '';
}
return token
},
async session({ session, token }) {
session.accessToken = token.accessToken as string
session['user']['username'] = token.username as string
session.accessToken = token.accessToken as string;
session['user']['username'] = token.username as string;
return session
}
}
})
},
})

View File

@@ -6,13 +6,14 @@ interface ErrorResponse {
}
export interface Faucet {
address: string
secret: string
xrp: number
hash: string
code: string
address: string;
secret: string;
xrp: number;
hash: string;
code: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Faucet | ErrorResponse>
@@ -20,25 +21,20 @@ export default async function handler(
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed!' })
}
const { account } = req.query
const ip = Array.isArray(req?.headers?.['x-real-ip'])
? req?.headers?.['x-real-ip'][0]
: req?.headers?.['x-real-ip']
const { account } = req.query;
const ip = Array.isArray(req?.headers?.["x-real-ip"]) ? req?.headers?.["x-real-ip"][0] : req?.headers?.["x-real-ip"];
try {
const response = await fetch(
`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`,
{
method: 'POST',
headers: {
'x-forwarded-for': ip || ''
}
}
)
const json: Faucet | ErrorResponse = await response.json()
if ('error' in json) {
const response = await fetch(`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`, {
method: 'POST',
headers: {
'x-forwarded-for': ip || '',
},
});
const json: Faucet | ErrorResponse = await response.json();
if ("error" in json) {
return res.status(429).json(json)
}
return res.status(200).json(json)
return res.status(200).json(json);
} catch (err) {
console.log(err)
return res.status(500).json({ error: 'Server error' })

View File

@@ -5,6 +5,9 @@ type Data = {
name: string
}
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

View File

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

View File

@@ -1,59 +1,63 @@
import dynamic from 'next/dynamic'
import React from 'react'
import Split from 'react-split'
import { useSnapshot } from 'valtio'
import state from '../../state'
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
import dynamic from "next/dynamic";
import React from "react";
import Split from "react-split";
import { useSnapshot } from "valtio";
import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
const DeployEditor = dynamic(() => import('../../components/DeployEditor'), {
ssr: false
})
const DeployEditor = dynamic(() => import("../../components/DeployEditor"), {
ssr: false,
});
const Accounts = dynamic(() => import('../../components/Accounts'), {
ssr: false
})
const Accounts = dynamic(() => import("../../components/Accounts"), {
ssr: false,
});
const LogBox = dynamic(() => import('../../components/LogBox'), {
ssr: false
})
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Deploy = () => {
const { deployLogs } = useSnapshot(state)
const { deployLogs } = useSnapshot(state);
return (
<Split
direction="vertical"
gutterSize={4}
gutterAlign="center"
sizes={getSplit('deployVertical') || [40, 60]}
style={{ height: 'calc(100vh - 60px)' }}
onDragEnd={e => saveSplit('deployVertical', e)}
sizes={getSplit("deployVertical") || [40, 60]}
style={{ height: "calc(100vh - 60px)" }}
onDragEnd={(e) => saveSplit("deployVertical", e)}
>
<main style={{ display: 'flex', flex: 1, position: 'relative' }}>
<main style={{ display: "flex", flex: 1, position: "relative" }}>
<DeployEditor />
</main>
<Split
direction="horizontal"
sizes={getSplit('deployHorizontal') || [50, 50]}
sizes={getSplit("deployHorizontal") || [50, 50]}
minSize={[320, 160]}
gutterSize={4}
gutterAlign="center"
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
height: '100%'
display: "flex",
flexDirection: "row",
width: "100%",
height: "100%",
}}
onDragEnd={e => saveSplit('deployHorizontal', e)}
onDragEnd={(e) => saveSplit("deployHorizontal", e)}
>
<div style={{ alignItems: 'stretch', display: 'flex' }}>
<div style={{ alignItems: "stretch", display: "flex" }}>
<Accounts />
</div>
<div>
<LogBox title="Deploy Log" logs={deployLogs} clearLog={() => (state.deployLogs = [])} />
<LogBox
title="Deploy Log"
logs={deployLogs}
clearLog={() => (state.deployLogs = [])}
/>
</div>
</Split>
</Split>
)
}
);
};
export default Deploy
export default Deploy;

View File

@@ -1,251 +1,83 @@
import { Label } from '@radix-ui/react-label'
import type { NextPage } from 'next'
import dynamic from 'next/dynamic'
import { FileJs, Gear, Play } from 'phosphor-react'
import Hotkeys from 'react-hot-keys'
import Split from 'react-split'
import { useSnapshot } from 'valtio'
import { ButtonGroup, Flex } from '../../components'
import Box from '../../components/Box'
import Button from '../../components/Button'
import Popover from '../../components/Popover'
import RunScript from '../../components/RunScript'
import state, { IFile } from '../../state'
import { compileCode } from '../../state/actions'
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
import { styled } from '../../stitches.config'
import { getFileExtention } from '../../utils/helpers'
import type { NextPage } from "next";
import dynamic from "next/dynamic";
import { Play } from "phosphor-react";
import Hotkeys from "react-hot-keys";
import Split from "react-split";
import { useSnapshot } from "valtio";
import Box from "../../components/Box";
import Button from "../../components/Button";
import state from "../../state";
import { compileCode } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
const HooksEditor = dynamic(() => import('../../components/HooksEditor'), {
ssr: false
})
const LogBox = dynamic(() => import('../../components/LogBox'), {
ssr: false
})
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
ssr: false,
});
const OptimizationText = () => (
<span>
Specify which optimization level to use for compiling. For example -O0 means no optimization:
this level compiles the fastest and generates the most debuggable code. -O2 means moderate level
of optimization which enables most optimizations. Read more about the options from{' '}
<a
className="link"
rel="noopener noreferrer"
target="_blank"
href="https://clang.llvm.org/docs/CommandGuide/clang.html#cmdoption-o0"
>
clang documentation
</a>
.
</span>
)
const StyledOptimizationText = styled(OptimizationText, {
color: '$mauve12 !important',
fontSize: '200px',
'span a.link': {
color: 'red'
}
})
const CompilerSettings = () => {
const snap = useSnapshot(state)
return (
<Flex css={{ minWidth: 200, flexDirection: 'column', gap: '$5' }}>
<Box>
<Label
style={{
flexDirection: 'row',
display: 'flex'
}}
>
Optimization level{' '}
<Popover
css={{
maxWidth: '240px',
lineHeight: '1.3',
a: {
color: '$purple11'
},
'.dark &': {
backgroundColor: '$black !important',
'.arrow': {
fill: '$colors$black'
}
}
}}
content={<StyledOptimizationText />}
>
<Flex
css={{
position: 'relative',
top: '-1px',
ml: '$1',
backgroundColor: '$mauve8',
borderRadius: '$full',
cursor: 'pointer',
width: '16px',
height: '16px',
alignItems: 'center',
justifyContent: 'center'
}}
>
?
</Flex>
</Popover>
</Label>
<ButtonGroup css={{ mt: '$2', fontFamily: '$monospace' }}>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O0'}
onClick={() => (state.compileOptions.optimizationLevel = '-O0')}
>
-O0
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O1'}
onClick={() => (state.compileOptions.optimizationLevel = '-O1')}
>
-O1
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O2'}
onClick={() => (state.compileOptions.optimizationLevel = '-O2')}
>
-O2
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O3'}
onClick={() => (state.compileOptions.optimizationLevel = '-O3')}
>
-O3
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O4'}
onClick={() => (state.compileOptions.optimizationLevel = '-O4')}
>
-O4
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-Os'}
onClick={() => (state.compileOptions.optimizationLevel = '-Os')}
>
-Os
</Button>
</ButtonGroup>
</Box>
</Flex>
)
}
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Home: NextPage = () => {
const snap = useSnapshot(state)
const snap = useSnapshot(state);
const activeFile = snap.files[snap.active] as IFile | undefined
const activeFileExt = getFileExtention(activeFile?.name)
const canCompile = activeFileExt === 'c' || activeFileExt === 'wat'
return (
<Split
direction="vertical"
sizes={getSplit('developVertical') || [70, 30]}
sizes={getSplit("developVertical") || [70, 30]}
minSize={[100, 100]}
gutterAlign="center"
gutterSize={4}
style={{ height: 'calc(100vh - 60px)' }}
onDragEnd={e => saveSplit('developVertical', e)}
style={{ height: "calc(100vh - 60px)" }}
onDragEnd={(e) => saveSplit("developVertical", e)}
>
<main style={{ display: 'flex', flex: 1, position: 'relative' }}>
<main style={{ display: "flex", flex: 1, position: "relative" }}>
<HooksEditor />
{canCompile && (
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
"c" && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
onKeyDown={() =>
!snap.compiling && snap.files.length && compileCode(snap.active)
}
>
<Flex
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: 'absolute',
bottom: '$4',
left: '$4',
alignItems: 'center',
display: 'flex',
cursor: 'pointer',
gap: '$2'
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
<Popover content={<CompilerSettings />}>
<Button variant="primary" css={{ px: '10px' }}>
<Gear size="16px" />
</Button>
</Popover>
</Flex>
</Hotkeys>
)}
{activeFileExt === 'js' && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
>
<Flex
css={{
position: 'absolute',
bottom: '$4',
left: '$4',
alignItems: 'center',
display: 'flex',
cursor: 'pointer',
gap: '$2'
}}
>
<RunScript file={activeFile as IFile} />
</Flex>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Hotkeys>
)}
</main>
<Flex css={{ width: '100%' }}>
<Flex
css={{
flex: 1,
background: '$mauve1',
position: 'relative',
borderRight: '1px solid $mauve8'
}}
>
<LogBox title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} />
</Flex>
{activeFileExt === 'js' && (
<Flex
css={{
flex: 1
}}
>
<LogBox
Icon={FileJs}
title="Script Log"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
/>
</Flex>
)}
</Flex>
<Box
css={{
display: "flex",
background: "$mauve1",
position: "relative",
}}
>
<LogBox
title="Development Log"
clearLog={() => (state.logs = [])}
logs={snap.logs}
/>
</Box>
</Split>
)
}
);
};
export default Home
export default Home;

View File

@@ -1,5 +1,5 @@
const Home = () => {
return <p>homepage</p>
}
return <p>homepage</p>;
};
export default Home
export default Home;

View File

@@ -1,37 +1,38 @@
import { useEffect } from 'react'
import { signIn, useSession } from 'next-auth/react'
import { useEffect } from "react";
import { signIn, useSession } from "next-auth/react";
import Box from '../components/Box'
import Spinner from '../components/Spinner'
import Box from "../components/Box";
import Spinner from "../components/Spinner";
const SignInPage = () => {
const { data: session, status } = useSession()
const { data: session, status } = useSession();
useEffect(() => {
if (status !== 'loading' && !session) void signIn('github', { redirect: false })
if (status !== 'loading' && session) window.close()
}, [session, status])
if (status !== "loading" && !session)
void signIn("github", { redirect: false });
if (status !== "loading" && session) window.close();
}, [session, status]);
return (
<Box
css={{
display: 'flex',
backgroundColor: '$mauve1',
position: 'absolute',
display: "flex",
backgroundColor: "$mauve1",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: 9999,
textAlign: 'center',
justifyContent: 'center',
alignItems: 'center',
gap: '$2'
textAlign: "center",
justifyContent: "center",
alignItems: "center",
gap: "$2",
}}
>
Logging in <Spinner />
</Box>
)
}
);
};
export default SignInPage
export default SignInPage;

View File

@@ -1,134 +1,431 @@
import dynamic from 'next/dynamic'
import Split from 'react-split'
import { useSnapshot } from 'valtio'
import { Box, Container, Flex, Tab, Tabs } from '../../components'
import Transaction from '../../components/Transaction'
import state, { renameTxState } from '../../state'
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
import { transactionsState, modifyTxState } from '../../state'
import { useEffect, useState } from 'react'
import { FileJs } from 'phosphor-react'
import RunScript from '../../components/RunScript'
import dynamic from "next/dynamic";
import { Play } from "phosphor-react";
import { FC, useCallback, useEffect, useState } from "react";
import Split from "react-split";
import { useSnapshot } from "valtio";
import {
Box,
Button,
Container,
Flex,
Input,
Select,
Tab,
Tabs,
Text,
} from "../../components";
import transactionsData from "../../content/transactions.json";
import state from "../../state";
import { sendTransaction } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
const DebugStream = dynamic(() => import('../../components/DebugStream'), {
ssr: false
})
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false,
});
const LogBox = dynamic(() => import('../../components/LogBox'), {
ssr: false
})
const Accounts = dynamic(() => import('../../components/Accounts'), {
ssr: false
})
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Accounts = dynamic(() => import("../../components/Accounts"), {
ssr: false,
});
// type SelectOption<T> = { value: T, label: string };
type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
interface Props {
header?: string;
}
const Transaction: FC<Props> = ({ header, ...props }) => {
const snap = useSnapshot(state);
const transactionsOptions = transactionsData.map((tx) => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const [selectedTransaction, setSelectedTransaction] = useState<
typeof transactionsOptions[0] | null
>(null);
const accountOptions = snap.accounts.map((acc) => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState<
typeof accountOptions[0] | null
>(null);
const destAccountOptions = snap.accounts
.map((acc) => ({
label: acc.name,
value: acc.address,
}))
.filter((acc) => acc.value !== selectedAccount?.value);
const [selectedDestAccount, setSelectedDestAccount] = useState<
typeof destAccountOptions[0] | null
>(null);
const [txIsLoading, setTxIsLoading] = useState(false);
const [txIsDisabled, setTxIsDisabled] = useState(false);
const [txFields, setTxFields] = useState<TxFields>({});
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = snap.accounts.find(
(acc) => acc.address === selectedAccount?.value
);
if (!account || !transactionType || txIsLoading) {
setTxIsDisabled(true);
} else {
setTxIsDisabled(false);
}
}, [txIsLoading, selectedTransaction, selectedAccount, snap.accounts]);
useEffect(() => {
let _txFields: TxFields | undefined = transactionsData.find(
(tx) => tx.TransactionType === selectedTransaction?.value
);
if (!_txFields) return setTxFields({});
_txFields = { ..._txFields } as TxFields;
setSelectedDestAccount(null);
// @ts-ignore
delete _txFields.TransactionType;
// @ts-ignore
delete _txFields.Account;
// @ts-ignore
delete _txFields.Sequence;
setTxFields(_txFields);
}, [selectedTransaction, setSelectedDestAccount]);
const submitTest = useCallback(async () => {
const account = snap.accounts.find(
(acc) => acc.address === selectedAccount?.value
);
const TransactionType = selectedTransaction?.value;
if (!account || !TransactionType || txIsDisabled) return;
setTxIsLoading(true);
// setTxIsError(null)
try {
let options = { ...txFields };
options.Destination = selectedDestAccount?.value;
(Object.keys(options) as (keyof TxFields)[]).forEach((field) => {
let _value = options[field];
// convert currency
if (typeof _value === "object" && _value.type === "currency") {
if (+_value.value) {
options[field] = (+_value.value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (typeof _value === "object" && _value.type === "json") {
if (typeof _value.value === "object") {
options[field] = _value.value as any;
} else {
try {
options[field] = JSON.parse(_value.value);
} catch (error) {
const message = `Input error for json field '${field}': ${
error instanceof Error ? error.message : ""
}`;
throw Error(message);
}
}
}
// delete unneccesary fields
if (!options[field]) {
delete options[field];
}
});
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
await sendTransaction(
account,
{
TransactionType,
...options,
},
{ logPrefix }
);
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({ type: "error", message: error.message });
}
}
setTxIsLoading(false);
}, [
header,
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
snap.accounts,
txFields,
txIsDisabled,
]);
const resetState = useCallback(() => {
setSelectedAccount(null);
setSelectedDestAccount(null);
setSelectedTransaction(null);
setTxFields({});
setTxIsDisabled(false);
setTxIsLoading(false);
}, []);
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(
(k) => !usualFields.includes(k)
) as OtherFields;
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={(tt) => setSelectedTransaction(tt as any)}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={(acc) => setSelectedAccount(acc as any)}
/>
</Flex>
{txFields.Amount !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={(e) =>
setTxFields({
...txFields,
Amount: { type: "currency", value: e.target.value },
})
}
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc) => setSelectedDestAccount(acc as any)}
/>
</Flex>
)}
{otherFields.map((field) => {
let _value = txFields[field];
let value = typeof _value === "object" ? _value.value : _value;
value =
typeof value === "object"
? JSON.stringify(value)
: value?.toLocaleString();
let isCurrency =
typeof _value === "object" && _value.type === "currency";
return (
<Flex
key={field}
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isCurrency ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={(e) =>
setTxFields({
...txFields,
[field]:
typeof _value === "object"
? { ..._value, value: e.target.value }
: e.target.value,
})
}
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
);
})}
</Flex>
</Container>
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
}}
>
<Button outline>VIEW AS JSON</Button>
<Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
);
};
const Test = () => {
// This and useEffect is here to prevent useLayoutEffect warnings from react-split
const [showComponent, setShowComponent] = useState(false)
const { transactionLogs } = useSnapshot(state)
const { transactions, activeHeader } = useSnapshot(transactionsState)
const snap = useSnapshot(state)
useEffect(() => {
setShowComponent(true)
}, [])
if (!showComponent) {
return null
}
const hasScripts = Boolean(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>
)
const { transactionLogs } = useSnapshot(state);
const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
return (
<Container css={{ px: 0 }}>
<Split
direction="vertical"
sizes={
hasScripts && getSplit('testVertical')?.length === 2
? [50, 20, 30]
: hasScripts
? [50, 20, 50]
: [50, 50]
}
sizes={getSplit("testVertical") || [50, 50]}
gutterSize={4}
gutterAlign="center"
style={{ height: 'calc(100vh - 60px)' }}
onDragEnd={e => saveSplit('testVertical', e)}
style={{ height: "calc(100vh - 60px)" }}
onDragEnd={(e) => saveSplit("testVertical", e)}
>
<Flex
row
fluid
css={{
justifyContent: 'center',
p: '$3 $2'
justifyContent: "center",
p: "$3 $2",
}}
>
<Split
direction="horizontal"
sizes={getSplit('testHorizontal') || [50, 50]}
sizes={getSplit("testHorizontal") || [50, 50]}
minSize={[180, 320]}
gutterSize={4}
gutterAlign="center"
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
height: '100%'
display: "flex",
flexDirection: "row",
width: "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
label="Transaction"
activeHeader={activeHeader}
// TODO make header a required field
onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header
}}
keepAllAlive
defaultExtension="json"
allowedExtensions={['json']}
onCreateNewTab={header => modifyTxState(header, {})}
onRenameTab={(idx, nwName, oldName = '') => renameTxState(oldName, nwName)}
onCloseTab={(idx, header) => header && modifyTxState(header, undefined)}
forceDefaultExtension
defaultExtension=".json"
onCreateNewTab={(name) =>
setTabHeaders(tabHeaders.concat(name))
}
onCloseTab={(index) =>
setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))
}
>
{transactions.map(({ header, state }) => (
{tabHeaders.map((header) => (
<Tab key={header} header={header}>
<Transaction state={state} header={header} />
<Transaction header={header} />
</Tab>
))}
</Tabs>
</Box>
<Box css={{ width: '45%', mx: '$2', height: '100%' }}>
<Box css={{ width: "45%", mx: "$2", height: "100%" }}>
<Accounts card hideDeployBtn showHookStats />
</Box>
</Split>
</Flex>
{hasScripts ? (
<Flex
as="div"
css={{
borderTop: '1px solid $mauve6',
background: '$mauve1',
flexDirection: 'column'
}}
>
<LogBox
Icon={FileJs}
title="Helper scripts"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
renderNav={renderNav}
/>
</Flex>
) : null}
<Flex>
<Flex row fluid>
<Split
direction="horizontal"
sizes={[50, 50]}
@@ -136,16 +433,16 @@ const Test = () => {
gutterSize={4}
gutterAlign="center"
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
height: '100%'
display: "flex",
flexDirection: "row",
width: "100%",
height: "100%",
}}
>
<Box
css={{
borderRight: '1px solid $mauve8',
height: '100%'
borderRight: "1px solid $mauve8",
height: "100%",
}}
>
<LogBox
@@ -154,14 +451,14 @@ const Test = () => {
clearLog={() => (state.transactionLogs = [])}
/>
</Box>
<Box css={{ height: '100%' }}>
<Box css={{ height: "100%" }}>
<DebugStream />
</Box>
</Split>
</Flex>
</Split>
</Container>
)
}
);
};
export default Test
export default Test;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,19 @@
{
"name": "Hooks Builder",
"short_name": "Hooks Builder",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#161618",
"background_color": "#161618",
"display": "standalone"
"name": "Hooks Builder",
"short_name": "Hooks Builder",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#161618",
"background_color": "#161618",
"display": "standalone"
}

13
raw-loader.d.ts vendored
View File

@@ -1,9 +1,4 @@
declare module '*.md' {
const content: string
export default content
}
declare module '*.hook-bundle.js' {
const content: string
export default content
}
declare module "*.md" {
const content: string;
export default content;
};

View File

@@ -1,24 +1,25 @@
import toast from 'react-hot-toast'
import state, { FaucetAccountRes } from '../index'
import toast from "react-hot-toast";
import state, { FaucetAccountRes } from '../index';
export const names = [
'Alice',
'Bob',
'Carol',
'Carlos',
'Charlie',
'Dan',
'Dave',
'David',
'Faythe',
'Frank',
'Grace',
'Heidi',
'Judy',
'Olive',
'Peggy',
'Walter'
]
"Alice",
"Bob",
"Carol",
"Carlos",
"Charlie",
"Dan",
"Dave",
"David",
"Faythe",
"Frank",
"Grace",
"Heidi",
"Judy",
"Olive",
"Peggy",
"Walter",
];
/* This function adds faucet account to application global state.
* It calls the /api/faucet endpoint which in send a HTTP POST to
@@ -26,63 +27,67 @@ export const names = [
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
* is protected with CORS so that's why we did our own endpoint
*/
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
if (typeof window === undefined) return
const toastId = showToast ? toast.loading('Creating account') : ''
const res = await fetch(`${window.location.origin}/api/faucet`, {
method: 'POST'
})
const json: FaucetAccountRes | { error: string } = await res.json()
if ('error' in json) {
if (showToast) {
return toast.error(json.error, { id: toastId })
} else {
return
}
} 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'
})
export const addFaucetAccount = async (showToast: boolean = false) => {
// Lets limit the number of faucet accounts to 5 for now
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 res = await fetch(`${window.location.origin}/api/faucet`, {
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
if (showToast) {
return toast.error(json.error, { id: toastId });
} else {
return;
}
} else {
if (showToast) {
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'
});
}
}
};
// fetch initial faucets
;(async function fetchFaucets() {
(async function fetchFaucets() {
if (typeof window !== 'undefined') {
if (state.accounts.length === 0) {
await addFaucetAccount()
await addFaucetAccount();
// setTimeout(() => {
// addFaucetAccount();
// }, 10000);
}
}
})()
})();
export const addFunds = async (address: string) => {
const toastId = toast.loading('Requesting funds')
const toastId = toast.loading("Requesting funds");
const res = await fetch(`${window.location.origin}/api/faucet?account=${address}`, {
method: 'POST'
})
const json: FaucetAccountRes | { error: string } = await res.json()
if ('error' in json) {
return toast.error(json.error, { id: toastId })
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
return toast.error(json.error, { id: toastId });
} else {
toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId })
const currAccount = state.accounts.find(acc => acc.address === address)
toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId });
const currAccount = state.accounts.find(acc => acc.address === address);
if (currAccount) {
currAccount.xrp = (Number(currAccount.xrp) + json.xrp * 1000000).toString()
currAccount.xrp = (Number(currAccount.xrp) + (json.xrp * 1000000)).toString();
}
}
}

View File

@@ -1,171 +1,110 @@
import toast from 'react-hot-toast'
import Router from 'next/router'
import Router from 'next/router';
import toast from "react-hot-toast";
import { ref } from "valtio";
import { decodeBinary } from "../../utils/decodeBinary";
import state from "../index";
import { saveFile } from "./saveFile";
import state from '../index'
import { saveFile } from './saveFile'
import { decodeBinary } from '../../utils/decodeBinary'
import { ref } from 'valtio'
/* compileCode sends the code of the active file to compile endpoint
* If all goes well you will get base64 encoded wasm file back with
* some extra logging information if we can provide it. This function
* some extra logging information if we can provide it. This function
* also decodes the returned wasm and creates human readable WAT file
* out of it and store both in global state.
*/
export const compileCode = async (activeId: number) => {
// Save the file to global state
saveFile(false, activeId)
const file = state.files[activeId]
if (file.name.endsWith('.wat')) {
return compileWat(activeId)
}
saveFile(false);
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error('Missing env!')
throw Error("Missing env!");
}
// Bail out if we're already compiling
if (state.compiling) {
// if compiling is ongoing return // TODO Inform user about it.
return
// if compiling is ongoing return
return;
}
// Set loading state to true
state.compiling = true
state.compiling = true;
// Reset development log
state.logs = []
try {
file.containsErrors = false
let res: Response
try {
res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
output: 'wasm',
compress: true,
strip: state.compileOptions.strip,
files: [
{
type: 'c',
options: state.compileOptions.optimizationLevel || '-O2',
name: file.name,
src: file.content
}
]
})
})
} catch (error) {
throw Error('Something went wrong, check your network connection and try again!')
}
const json = await res.json()
state.compiling = false
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
output: "wasm",
compress: true,
files: [
{
type: "c",
name: state.files[activeId].name,
src: state.files[activeId].content,
},
],
}),
});
const json = await res.json();
state.compiling = false;
if (!json.success) {
const errors = [json.message]
state.logs.push({ type: "error", message: json.message });
if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => {
if (!task.success) {
errors.push(task?.console)
state.logs.push({ type: "error", message: task?.console });
}
})
});
}
throw errors
}
try {
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output)
// Import wabt from and create human readable version of wasm file and
// put it into state
const ww = await (await import('wabt')).default()
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true
})
myModule.applyNames()
const wast = myModule.toText({ foldExprs: false, inlineExport: false })
file.compiledContent = ref(bufferData)
file.lastCompiled = new Date()
file.compiledValueSnapshot = file.content
file.compiledWatContent = wast
} catch (error) {
throw Error('Invalid compilation result produced, check your code for errors and try again!')
return toast.error(`Couldn't compile!`, { position: "bottom-center" });
}
toast.success('Compiled successfully!', { position: 'bottom-center' })
state.logs.push({
type: 'success',
type: "success",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace('develop', 'deploy'),
linkText: 'Go to deploy'
})
} catch (err) {
console.log(err)
if (err instanceof Array && typeof err[0] === 'string') {
err.forEach(message => {
state.logs.push({
type: 'error',
message
})
})
} else if (err instanceof Error) {
state.logs.push({
type: 'error',
message: err.message
})
} else {
state.logs.push({
type: 'error',
message: 'Something went wrong, come back later!'
})
}
state.compiling = false
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
file.containsErrors = true
}
}
export const compileWat = async (activeId: number) => {
if (state.compiling) return;
const file = state.files[activeId]
state.compiling = true
state.logs = []
try {
const wabt = await (await import('wabt')).default()
const module = wabt.parseWat(file.name, file.content);
module.resolveNames();
module.validate();
const { buffer } = module.toBinary({
log: false,
write_debug_names: true,
link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy",
});
file.compiledContent = ref(buffer)
file.lastCompiled = new Date()
file.compiledValueSnapshot = file.content
file.compiledWatContent = file.content
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
state.files[state.active].compiledContent = ref(bufferData);
state.files[state.active].lastCompiled = new Date();
toast.success('Compiled successfully!', { position: 'bottom-center' })
state.logs.push({
type: 'success',
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace('develop', 'deploy'),
linkText: 'Go to deploy'
})
} catch (err) {
console.log(err)
let message = "Error compiling WAT file!"
if (err instanceof Error) {
message = err.message
// Import wabt from and create human readable version of wasm file and
// put it into state
import("wabt").then((wabt) => {
const ww = wabt.default();
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true,
});
myModule.applyNames();
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
state.files[state.active].compiledWatContent = wast;
toast.success("Compiled successfully!", { position: "bottom-center" });
});
} catch (err: any) {
const error = err as Error
// TODO: Centralized error handling? Just a thought
if(error.message.includes("Failed to fetch")) {
state.logs.push({
type: "error",
message: "No connection to the compiler server!",
});
} else {
state.logs.push({
type: "error",
message: "Error occured while compiling!",
});
}
state.logs.push({
type: 'error',
message
})
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
file.containsErrors = true
state.compiling = false;
}
state.compiling = false
}
};

View File

@@ -1,29 +1,17 @@
import { getFileExtention } from '../../utils/helpers'
import state, { IFile } from '../index'
const languageMapping: Record<string, string | undefined> = {
ts: 'typescript',
js: 'javascript',
md: 'markdown',
c: 'c',
h: 'c',
txt: 'text'
}
import state, { IFile } from '../index';
const languageMapping = {
'ts': 'typescript',
'js': 'javascript',
'md': 'markdown',
'c': 'c',
'h': 'c',
'other': ''
} /* Initializes empty file to global state */
export const createNewFile = (name: string) => {
const ext = getFileExtention(name) || ''
const emptyFile: IFile = { name, language: languageMapping[ext] || 'text', content: '' }
state.files.push(emptyFile)
state.active = state.files.length - 1
}
export const renameFile = (oldName: string, nwName: string) => {
const file = state.files.find(file => file.name === oldName)
if (!file) throw Error(`No file exists with name ${oldName}`)
const ext = getFileExtention(nwName) || ''
const language = languageMapping[ext] || 'text'
file.name = nwName
file.language = language
}
const tempName = name.split('.');
const fileExt = tempName[tempName.length - 1] || 'other';
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
state.files.push(emptyFile);
state.active = state.files.length - 1;
};

View File

@@ -1,24 +0,0 @@
import state, { transactionsState } from '..'
export const deleteAccount = (addr?: string) => {
if (!addr) return
const index = state.accounts.findIndex(acc => acc.address === addr)
if (index === -1) return
state.accounts.splice(index, 1)
// update selected accounts
transactionsState.transactions
.filter(t => t.state.selectedAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedAccount
if (!acc) return
acc.label = acc.value
})
transactionsState.transactions
.filter(t => t.state.selectedDestAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedDestAccount
if (!acc) return
acc.label = acc.value
})
}

228
state/actions/deployHook.ts Normal file
View File

@@ -0,0 +1,228 @@
import toast from "react-hot-toast";
import { derive, sign } from "xrpl-accountlib";
import { AnyJson } from "xrpl-client";
import { SetHookData } from "../../components/SetHookDialog";
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
import state, { IAccount } from "../index";
const hash = async (string: string) => {
const utf8 = new TextEncoder().encode(string);
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
return hashHex;
}
function toHex(str: string) {
var result = '';
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16);
}
return result.toUpperCase();
}
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
if (!arrayBuffer) {
return "";
}
if (
typeof arrayBuffer !== "object" ||
arrayBuffer === null ||
typeof arrayBuffer.byteLength !== "number"
) {
throw new TypeError("Expected input to be an ArrayBuffer");
}
var view = new Uint8Array(arrayBuffer);
var result = "";
var value;
for (var i = 0; i < view.length; i++) {
value = view[i].toString(16);
result += value.length === 1 ? "0" + value : value;
}
return result;
}
/* deployHook function turns the wasm binary into
* hex string, signs the transaction and deploys it to
* Hooks testnet.
*/
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData): Promise<AnyJson | undefined> => {
if (
!state.files ||
state.files.length === 0 ||
!state.files?.[state.active]?.compiledContent
) {
console.log("ebin1")
return;
}
if (!state.files?.[state.active]?.compiledContent) {
console.log("ebin2")
return;
}
if (!state.client) {
console.log("ebin3")
return;
}
const HookNamespace = await hash(arrayBufferToHex(
state.files?.[state.active]?.compiledContent
).toUpperCase());
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value);
const { HookParameters } = data;
const filteredHookParameters = HookParameters.filter(hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue)?.map(aa => ({ HookParameter: { HookParameterName: toHex(aa.HookParameter.HookParameterName || ''), HookParameterValue: toHex(aa.HookParameter.HookParameterValue || '') } }));
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
// return {
// HookGrant: {
// ...(hg.HookGrant.Authorize && { Authorize: hg.HookGrant.Authorize }),
// // HookHash: hg.HookGrant.HookHash || undefined
// ...(hg.HookGrant.HookHash && { HookHash: hg.HookGrant.HookHash })
// }
// }
// });
if (typeof window !== "undefined") {
const tx = {
Account: account.address,
TransactionType: "SetHook",
Sequence: account.sequence,
Fee: "100000",
Hooks: [
{
Hook: {
CreateCode: arrayBufferToHex(
state.files?.[state.active]?.compiledContent
).toUpperCase(),
HookOn: calculateHookOn(hookOnValues),
HookNamespace,
HookApiVersion: 0,
Flags: 1,
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
...(filteredHookParameters.length > 0 && { HookParameters: filteredHookParameters }),
}
}
]
};
const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currentAccount) {
currentAccount.isLoading = true;
}
let submitRes;
try {
submitRes = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
if (submitRes.engine_result === "tesSUCCESS") {
state.deployLogs.push({
type: "success",
message: "Hook deployed successfully ✅",
});
state.deployLogs.push({
type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
});
} else {
state.deployLogs.push({
type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`,
});
}
} catch (err) {
console.log("Ebin")
console.log(err);
state.deployLogs.push({
type: "error",
message: "Error occured while deploying",
});
}
if (currentAccount) {
currentAccount.isLoading = false;
}
return submitRes;
}
};
export const deleteHook = async (account: IAccount & { name?: string }) => {
if (!state.client) {
return;
}
const currentAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
return
}
if (typeof window !== "undefined") {
const tx = {
Account: account.address,
TransactionType: "SetHook",
Sequence: account.sequence,
Fee: "100000",
Hooks: [
{
Hook: {
CreateCode: "",
Flags: 1,
}
}
]
};
const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair);
if (currentAccount) {
currentAccount.isLoading = true;
}
let submitRes;
const toastId = toast.loading("Deleting hook...");
try {
submitRes = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
if (submitRes.engine_result === "tesSUCCESS") {
toast.success('Hook deleted successfully ✅', { id: toastId })
state.deployLogs.push({
type: "success",
message: "Hook deleted successfully ✅",
});
state.deployLogs.push({
type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
});
currentAccount.hooks = [];
} else {
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, { id: toastId })
state.deployLogs.push({
type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`,
});
}
} catch (err) {
console.log(err);
toast.error('Error occured while deleting hoook', { id: toastId })
state.deployLogs.push({
type: "error",
message: "Error occured while deleting hook",
});
}
if (currentAccount) {
currentAccount.isLoading = false;
}
return submitRes;
}
};

View File

@@ -1,274 +0,0 @@
import { derive, sign } from 'xrpl-accountlib'
import toast from 'react-hot-toast'
import state, { IAccount } from '../index'
import calculateHookOn, { TTS } from '../../utils/hookOnCalculator'
import { Link } from '../../components'
import { ref } from 'valtio'
import estimateFee from '../../utils/estimateFee'
import { SetHookData } from '../../utils/setHook'
import ResultLink from '../../components/ResultLink'
import { xrplSend } from './xrpl-client'
export const sha256 = async (string: string) => {
const utf8 = new TextEncoder().encode(string)
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(bytes => bytes.toString(16).padStart(2, '0')).join('')
return hashHex
}
function toHex(str: string) {
var result = ''
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16)
}
return result.toUpperCase()
}
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
if (!arrayBuffer) {
return ''
}
if (
typeof arrayBuffer !== 'object' ||
arrayBuffer === null ||
typeof arrayBuffer.byteLength !== 'number'
) {
throw new TypeError('Expected input to be an ArrayBuffer')
}
var view = new Uint8Array(arrayBuffer)
var result = ''
var value
for (var i = 0; i < view.length; i++) {
value = view[i].toString(16)
result += value.length === 1 ? '0' + value : value
}
return result
}
export const prepareDeployHookTx = async (
account: IAccount & { name?: string },
data: SetHookData
) => {
const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active]
: state.files.filter(file => file.compiledContent)[0]
if (!state.files || state.files.length === 0) {
return
}
if (!activeFile?.compiledContent) {
return
}
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase()
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value)
const { HookParameters } = data
const filteredHookParameters = HookParameters.filter(
hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
)?.map(aa => ({
HookParameter: {
HookParameterName: toHex(aa.HookParameter.HookParameterName || ''),
HookParameterValue: aa.HookParameter.HookParameterValue || ''
}
}))
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
// return {
// HookGrant: {
// ...(hg.HookGrant.Authorize && { Authorize: hg.HookGrant.Authorize }),
// // HookHash: hg.HookGrant.HookHash || undefined
// ...(hg.HookGrant.HookHash && { HookHash: hg.HookGrant.HookHash })
// }
// }
// });
if (typeof window === 'undefined') return;
const tx = {
Account: account.address,
TransactionType: 'SetHook',
Sequence: account.sequence,
Fee: data.Fee,
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID || state.client.getState().server.networkId,
Hooks: [
{
Hook: {
CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(),
HookOn: calculateHookOn(hookOnValues),
HookNamespace,
HookApiVersion: 0,
Flags: 1,
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
...(filteredHookParameters.length > 0 && {
HookParameters: filteredHookParameters
})
}
}
]
}
return tx
}
/* deployHook function turns the wasm binary into
* hex string, signs the transaction and deploys it to
* Hooks testnet.
*/
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
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)
if (!tx) {
return
}
const keypair = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, keypair)
const currentAccount = state.accounts.find(acc => acc.address === account.address)
if (currentAccount) {
currentAccount.isLoading = true
}
let submitRes
try {
submitRes = await xrplSend({
command: 'submit',
tx_blob: signedTransaction
})
const txHash = submitRes.tx_json?.hash
const resultMsg = ref(
<>
[<ResultLink result={submitRes.engine_result} />] {submitRes.engine_result_message}{' '}
{txHash && (
<>
Transaction hash:{' '}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
{txHash}
</Link>
</>
)}
</>
)
if (submitRes.engine_result === 'tesSUCCESS') {
state.deployLogs.push({
type: 'success',
message: 'Hook deployed successfully ✅'
})
state.deployLogs.push({
type: 'success',
message: resultMsg
})
} else if (submitRes.engine_result) {
state.deployLogs.push({
type: 'error',
message: resultMsg
})
} else {
state.deployLogs.push({
type: 'error',
message: `[${submitRes.error}] ${submitRes.error_exception}`
})
}
} catch (err) {
console.log(err)
state.deployLogs.push({
type: 'error',
message: 'Error occurred while deploying'
})
}
if (currentAccount) {
currentAccount.isLoading = false
}
return submitRes
}
}
export const deleteHook = async (account: IAccount & { name?: string }) => {
const currentAccount = state.accounts.find(acc => acc.address === account.address)
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
return
}
if (typeof window !== 'undefined') {
const tx = {
Account: account.address,
TransactionType: 'SetHook',
Sequence: account.sequence,
Fee: '100000',
Hooks: [
{
Hook: {
CreateCode: '',
Flags: 1
}
}
]
}
const keypair = derive.familySeed(account.secret)
try {
// Update tx Fee value with network estimation
const res = await estimateFee(tx, account)
tx['Fee'] = res?.base_fee ? res?.base_fee : '1000'
} catch (err) {
// use default value what you defined earlier
console.log(err)
}
const { signedTransaction } = sign(tx, keypair)
if (currentAccount) {
currentAccount.isLoading = true
}
let submitRes
const toastId = toast.loading('Deleting hook...')
try {
submitRes = await xrplSend({
command: 'submit',
tx_blob: signedTransaction
})
if (submitRes.engine_result === 'tesSUCCESS') {
toast.success('Hook deleted successfully ✅', { id: toastId })
state.deployLogs.push({
type: 'success',
message: 'Hook deleted successfully ✅'
})
state.deployLogs.push({
type: 'success',
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`
})
currentAccount.hooks = []
} else {
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, {
id: toastId
})
state.deployLogs.push({
type: 'error',
message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception
}`
})
}
} catch (err) {
console.log(err)
toast.error('Error occurred while deleting hook', { id: toastId })
state.deployLogs.push({
type: 'error',
message: 'Error occurred while deleting hook'
})
}
if (currentAccount) {
currentAccount.isLoading = false
}
return submitRes
}
}

View File

@@ -1,22 +1,19 @@
import { createZip } from '../../utils/zip'
import { guessZipFileName } from '../../utils/helpers'
import { createZip } from '../../utils/zip';
import { guessZipFileName } from '../../utils/helpers';
import state from '..'
import toast from 'react-hot-toast'
import toast from 'react-hot-toast';
export const downloadAsZip = async () => {
try {
state.zipLoading = true
// TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content }))
const wasmFiles = state.files
.filter(i => i.compiledContent)
.map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }))
const zipped = await createZip([...files, ...wasmFiles])
const zipFileName = guessZipFileName(files)
zipped.saveFile(zipFileName)
} catch (error) {
toast.error('Error occurred while creating zip file, try again later')
} finally {
state.zipLoading = false
}
}
try {
state.zipLoading = true
// TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content }));
const zipped = await createZip(files);
const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName);
} catch (error) {
toast.error('Error occured while creating zip file, try again later')
} finally {
state.zipLoading = false
}
};

View File

@@ -1,100 +1,82 @@
import { Octokit } from '@octokit/core'
import state, { IFile } from '../index'
import { templateFileIds } from '../constants'
import { Octokit } from "@octokit/core";
import Router from "next/router";
import state from '../index';
import { templateFileIds } from '../constants';
import { hookapiH, hookmacroH, sfcodesH } from '../constants/headerTemplates';
const octokit = new Octokit()
/**
* Fetches files from Github Gists based on gistId and stores them in global state
const octokit = new Octokit();
/* Fetches Gist files from Githug Gists based on
* gistId and stores the content in global state
*/
export const fetchFiles = async (gistId: string) => {
if (!gistId || state.files.length) return
export const fetchFiles = (gistId: string) => {
state.loading = true;
if (gistId && !state.files.length) {
state.logs.push({
type: "log",
message: `Fetching Gist with id: ${gistId}`,
});
state.loading = true
state.logs.push({
type: 'log',
message: `Fetching Gist with id: ${gistId}`
})
try {
const res = await octokit.request('GET /gists/{gist_id}', { gist_id: gistId })
const isTemplate = (id: string) =>
Object.values(templateFileIds)
.map(v => v.id)
.includes(id)
if (isTemplate(gistId)) {
// fetch headers
const headerRes = await fetch(
`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`
)
if (!headerRes.ok) throw Error('Failed to fetch headers')
const headerJson = await headerRes.json()
const headerFiles: Record<string, { filename: string; content: string; language: string }> =
{}
Object.entries(headerJson).forEach(([key, value]) => {
const fname = `${key}.h`
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
octokit
.request("GET /gists/{gist_id}", { gist_id: gistId })
.then(res => {
if (!Object.values(templateFileIds).includes(gistId)) {
return res
}
// in case of templates, fetch header file(s) and append to res
const files = {
...res.data.files,
'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: hookapiH, language: 'C' },
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: hookmacroH, language: 'C' },
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: sfcodesH, language: 'C' },
};
res.data.files = files;
return res;
// If you want to load templates from GIST instad, uncomment the code below and comment the code above.
// return octokit.request("GET /gists/{gist_id}", { gist_id: templateFileIds.headers }).then(({ data: { files: headerFiles } }) => {
// const files = { ...res.data.files, ...headerFiles }
// console.log(headerFiles)
// res.data.files = files
// return res
// })
})
const files = {
...res.data.files,
...headerFiles
}
res.data.files = files
}
if (!res.data.files) throw Error('No files could be fetched from given gist id!')
const files: IFile[] = Object.keys(res.data.files).map(filename => ({
name: res.data.files?.[filename]?.filename || 'untitled.c',
language: res.data.files?.[filename]?.language?.toLowerCase() || '',
content: res.data.files?.[filename]?.content || ''
}))
files.sort((a, b) => {
const aBasename = a.name.split('.')?.[0]
const aExt = a.name.split('.').pop() || ''
const bBasename = b.name.split('.')?.[0]
const bExt = b.name.split('.').pop() || ''
// default priority is undefined == 0
const extPriority: Record<string, number> = {
c: 3,
wat: 3,
md: 2,
h: -1
}
// Sort based on extention priorities
const comp = (extPriority[bExt] || 0) - (extPriority[aExt] || 0)
if (comp !== 0) return comp
// Otherwise fallback to alphabetical sorting
return aBasename.localeCompare(bBasename)
})
state.logs.push({
type: 'success',
message: 'Fetched successfully ✅'
})
state.files = files
state.gistId = gistId
state.gistOwner = res.data.owner?.login
const gistName =
files.find(file => file.language === 'c' || file.language === 'javascript')?.name ||
'untitled'
state.gistName = gistName
} catch (err) {
console.error(err)
let message: string
if (err instanceof Error) message = err.message
else message = `Something went wrong, try again later!`
state.logs.push({
type: 'error',
message: `Error: ${message}`
})
.then((res) => {
if (res.data.files && Object.keys(res.data.files).length > 0) {
const files = Object.keys(res.data.files).map((filename) => ({
name: res.data.files?.[filename]?.filename || "untitled.c",
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
content: res.data.files?.[filename]?.content || "",
}));
state.loading = false;
if (files.length > 0) {
state.logs.push({
type: "success",
message: "Fetched successfully ✅",
});
state.files = files;
state.gistId = gistId;
state.gistName = Object.keys(res.data.files)?.[0] || "untitled";
state.gistOwner = res.data.owner?.login;
return;
} else {
// Open main modal if now files
state.mainModalOpen = true;
}
return Router.push({ pathname: "/develop" });
}
state.loading = false;
})
.catch((err) => {
// console.error(err)
state.loading = false;
state.logs.push({
type: "error",
message: `Couldn't find Gist with id: ${gistId}`,
});
return;
});
return;
}
state.loading = false
}
state.loading = false;
};

View File

@@ -1,40 +1,30 @@
import toast from 'react-hot-toast'
import { derive, XRPL_Account } from 'xrpl-accountlib'
import toast from "react-hot-toast";
import { derive } from "xrpl-accountlib";
import state from '../index'
import { names } from './addFaucetAccount'
import state from '../index';
import { names } from './addFaucetAccount';
// Adds test account to global state with secret key
export const importAccount = (secret: string, name?: string) => {
export const importAccount = (secret: string) => {
if (!secret) {
return toast.error('You need to add secret!')
return toast.error("You need to add secret!");
}
if (state.accounts.find(acc => acc.secret === secret)) {
return toast.error('Account already added!')
if (state.accounts.find((acc) => acc.secret === secret)) {
return toast.error("Account already added!");
}
let account: XRPL_Account | null = null
try {
account = derive.familySeed(secret)
} catch (err: any) {
if (err?.message) {
toast.error(err.message)
} else {
toast.error('Error occurred while importing account')
}
return
}
if (!account || !account.secret.familySeed) {
return toast.error(`Couldn't create account!`)
const account = derive.familySeed(secret);
if (!account.secret.familySeed) {
return toast.error(`Couldn't create account!`);
}
state.accounts.push({
name: name || names[state.accounts.length],
address: account.address || '',
secret: account.secret.familySeed || '',
xrp: '0',
name: names[state.accounts.length],
address: account.address || "",
secret: account.secret.familySeed || "",
xrp: "0",
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
})
return toast.success('Account imported successfully!')
}
});
return toast.success("Account imported successfully!");
};

View File

@@ -1,14 +1,14 @@
import { addFaucetAccount } from './addFaucetAccount'
import { compileCode } from './compileCode'
import { createNewFile } from './createNewFile'
import { deployHook } from './deployHook'
import { fetchFiles } from './fetchFiles'
import { importAccount } from './importAccount'
import { saveFile } from './saveFile'
import { syncToGist } from './syncToGist'
import { updateEditorSettings } from './updateEditorSettings'
import { downloadAsZip } from './downloadAsZip'
import { sendTransaction } from './sendTransaction'
import { addFaucetAccount } from "./addFaucetAccount";
import { compileCode } from "./compileCode";
import { createNewFile } from "./createNewFile";
import { deployHook } from "./deployHook";
import { fetchFiles } from "./fetchFiles";
import { importAccount } from "./importAccount";
import { saveFile } from "./saveFile";
import { syncToGist } from "./syncToGist";
import { updateEditorSettings } from "./updateEditorSettings";
import { downloadAsZip } from "./downloadAsZip";
import { sendTransaction } from "./sendTransaction";
export {
addFaucetAccount,
@@ -22,4 +22,4 @@ export {
updateEditorSettings,
downloadAsZip,
sendTransaction
}
};

View File

@@ -1,5 +1,5 @@
import { snapshot } from 'valtio'
import state from '..'
import { snapshot } from "valtio"
import state from ".."
export type SplitSize = number[]
@@ -12,3 +12,4 @@ export const getSplit = (splitId: string): SplitSize | null => {
const split = splits[splitId]
return split ? split : null
}

View File

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

View File

@@ -0,0 +1,57 @@
import { derive, sign } from "xrpl-accountlib";
import state from '..'
import type { IAccount } from "..";
interface TransactionOptions {
TransactionType: string,
Account?: string,
Fee?: string,
Destination?: string
[index: string]: any
}
interface OtherOptions {
logPrefix?: string
}
export const sendTransaction = async (account: IAccount, txOptions: TransactionOptions, options?: OtherOptions) => {
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = "1000", ...opts } = txOptions
const tx: TransactionOptions = {
Account: account.address,
Sequence: account.sequence, // TODO auto-fillable
Fee, // TODO auto-fillable
...opts
};
const currAcc = state.accounts.find(acc => acc.address === account.address);
if (currAcc) {
currAcc.sequence = account.sequence + 1;
}
const { logPrefix = '' } = options || {}
try {
const signedAccount = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, signedAccount);
const response = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
if (response.engine_result === "tesSUCCESS") {
state.transactionLogs.push({
type: 'success',
message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}`
})
} else {
state.transactionLogs.push({
type: "error",
message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
});
}
} catch (err) {
console.error(err);
state.transactionLogs.push({
type: "error",
message: err instanceof Error ? `${logPrefix}Error: ${err.message}` : `${logPrefix}Something went wrong, try again later`,
});
}
};

View File

@@ -1,77 +0,0 @@
import { derive, sign } from 'xrpl-accountlib'
import state from '..'
import type { IAccount } from '..'
import ResultLink from '../../components/ResultLink'
import { ref } from 'valtio'
import { xrplSend } from './xrpl-client'
interface TransactionOptions {
TransactionType: string
Account?: string
Fee?: string
Destination?: string
[index: string]: any
}
interface OtherOptions {
logPrefix?: string
}
export const sendTransaction = async (
account: IAccount,
txOptions: TransactionOptions,
options?: OtherOptions
) => {
const { Fee = '1000', ...opts } = txOptions
const tx: TransactionOptions = {
Account: account.address,
Sequence: account.sequence,
Fee,
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID || state.client.getState().server.networkId,
...opts
}
const { logPrefix = '' } = options || {}
try {
const signedAccount = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, signedAccount)
const response = await xrplSend({
command: 'submit',
tx_blob: signedTransaction
})
const resultMsg = ref(
<>
{logPrefix}[<ResultLink result={response.engine_result} />] {response.engine_result_message}
</>
)
if (response.engine_result === 'tesSUCCESS') {
state.transactionLogs.push({
type: 'success',
message: resultMsg
})
} else if (response.engine_result) {
state.transactionLogs.push({
type: 'error',
message: resultMsg
})
} else {
state.transactionLogs.push({
type: 'error',
message: `${logPrefix}[${response.error}] ${response.error_exception}`
})
}
const currAcc = state.accounts.find(acc => acc.address === account.address)
if (currAcc && response.account_sequence_next) {
currAcc.sequence = response.account_sequence_next
}
} catch (err) {
console.error(err)
state.transactionLogs.push({
type: 'error',
message:
err instanceof Error
? `${logPrefix}Error: ${err.message}`
: `${logPrefix}Something went wrong, try again later`
})
}
}

View File

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

View File

@@ -1,97 +1,102 @@
import type { Session } from 'next-auth'
import toast from 'react-hot-toast'
import { Octokit } from '@octokit/core'
import Router from 'next/router'
import type { Session } from "next-auth";
import toast from "react-hot-toast";
import { Octokit } from "@octokit/core";
import Router from "next/router";
import state from '../index'
import { saveAllFiles } from './saveFile'
import state from '../index';
const octokit = new Octokit()
const octokit = new Octokit();
// Syncs the current files from the state to GitHub Gists.
export const syncToGist = async (session?: Session | null, createNewGist?: boolean) => {
saveAllFiles()
let files: Record<string, { filename: string; content: string }> = {}
state.gistLoading = true
export const syncToGist = async (
session?: Session | null,
createNewGist?: boolean
) => {
let files: Record<string, { filename: string; content: string }> = {};
state.gistLoading = true;
if (!session || !session.user) {
state.gistLoading = false
return toast.error('You need to be logged in!')
state.gistLoading = false;
return toast.error("You need to be logged in!");
}
const toastId = toast.loading('Pushing to Gist')
const toastId = toast.loading("Pushing to Gist");
if (!state.files || !state.files.length) {
state.gistLoading = false
state.gistLoading = false;
return toast.error(`You need to create some files we can push to gist`, {
id: toastId
})
id: toastId,
});
}
if (state.gistId && session?.user.username === state.gistOwner && !createNewGist) {
if (
state.gistId &&
session?.user.username === state.gistOwner &&
!createNewGist
) {
// You can only remove files from Gist by updating file with empty contents
// So we need to fetch existing files and compare those to local state
// and then send empty content if we don't have matching files anymore
// on local state
const currentFilesRes = await octokit.request('GET /gists/{gist_id}', {
gist_id: state.gistId
})
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", {
gist_id: state.gistId,
});
if (currentFilesRes.data.files) {
Object.keys(currentFilesRes?.data?.files).forEach(filename => {
files[`${filename}`] = { filename, content: '' }
})
Object.keys(currentFilesRes?.data?.files).forEach((filename) => {
files[`${filename}`] = { filename, content: "" };
});
}
state.files.forEach(file => {
files[`${file.name}`] = { filename: file.name, content: file.content }
})
state.files.forEach((file) => {
files[`${file.name}`] = { filename: file.name, content: file.content };
});
// Update existing Gist
octokit
.request('PATCH /gists/{gist_id}', {
.request("PATCH /gists/{gist_id}", {
gist_id: state.gistId,
files,
headers: {
authorization: `token ${session?.accessToken || ''}`
}
authorization: `token ${session?.accessToken || ""}`,
},
})
.then(res => {
state.gistLoading = false
return toast.success('Updated to gist successfully!', { id: toastId })
.then((res) => {
state.gistLoading = false;
return toast.success("Updated to gist successfully!", { id: toastId });
})
.catch(err => {
console.log(err)
state.gistLoading = false
.catch((err) => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not update Gist, try again later!`, {
id: toastId
})
})
id: toastId,
});
});
} else {
// Not Gist of the current user or it isn't Gist yet
state.files.forEach(file => {
files[`${file.name}`] = { filename: file.name, content: file.content }
})
state.files.forEach((file) => {
files[`${file.name}`] = { filename: file.name, content: file.content };
});
octokit
.request('POST /gists', {
.request("POST /gists", {
files,
public: true,
headers: {
authorization: `token ${session?.accessToken || ''}`
}
authorization: `token ${session?.accessToken || ""}`,
},
})
.then(res => {
state.gistLoading = false
state.gistOwner = res.data.owner?.login
state.gistId = res.data.id
.then((res) => {
state.gistLoading = false;
state.gistOwner = res.data.owner?.login;
state.gistId = res.data.id;
state.gistName = Array.isArray(res.data.files)
? Object.keys(res.data?.files)?.[0]
: 'Untitled'
Router.push({ pathname: `/develop/${res.data.id}` })
return toast.success('Created new gist successfully!', { id: toastId })
: "Untitled";
Router.push({ pathname: `/develop/${res.data.id}` });
return toast.success("Created new gist successfully!", { id: toastId });
})
.catch(err => {
console.log(err)
state.gistLoading = false
.catch((err) => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not create Gist, try again later!`, {
id: toastId
})
})
id: toastId,
});
});
}
}
};
export default syncToGist
export default syncToGist;

View File

@@ -1,12 +1,14 @@
import state, { IState } from '../index'
import state, { IState } from '../index';
// Updates editor settings and stores them
// in global state
export const updateEditorSettings = (editorSettings: IState['editorSettings']) => {
state.editorCtx?.getModels().forEach(model => {
export const updateEditorSettings = (
editorSettings: IState["editorSettings"]
) => {
state.editorCtx?.getModels().forEach((model) => {
model.updateOptions({
...editorSettings
})
})
return (state.editorSettings = editorSettings)
}
...editorSettings,
});
});
return (state.editorSettings = editorSettings);
};

View File

@@ -1,7 +0,0 @@
import { XrplClient } from 'xrpl-client';
import state from '..';
export const xrplSend = async(...params: Parameters<XrplClient['send']>) => {
const client = await state.client.ready()
return client.send(...params);
}

View File

@@ -1,79 +0,0 @@
import { SelectOption } from '../transactions';
interface Flags {
[key: string]: string;
}
export const transactionFlags: { [key: /* TransactionType */ string]: Flags } = {
"*": {
tfFullyCanonicalSig: '0x80000000'
},
Payment: {
tfNoDirectRipple: '0x00010000',
tfPartialPayment: '0x00020000',
tfLimitQuality: '0x00040000',
},
AccountSet: {
tfRequireDestTag: '0x00010000',
tfOptionalDestTag: '0x00020000',
tfRequireAuth: '0x00040000',
tfOptionalAuth: '0x00080000',
tfDisallowXRP: '0x00100000',
tfAllowXRP: '0x00200000',
},
NFTokenCreateOffer: {
tfSellNFToken: '0x00000001',
},
NFTokenMint: {
tfBurnable: '0x00000001',
tfOnlyXRP: '0x00000002',
tfTrustLine: '0x00000004',
tfTransferable: '0x00000008',
},
OfferCreate: {
tfPassive: '0x00010000',
tfImmediateOrCancel: '0x00020000',
tfFillOrKill: '0x00040000',
tfSell: '0x00080000',
},
PaymentChannelClaim: {
tfRenew: '0x00010000',
tfClose: '0x00020000',
},
TrustSet: {
tfSetfAuth: '0x00010000',
tfSetNoRipple: '0x00020000',
tfClearNoRipple: '0x00040000',
tfSetFreeze: '0x00100000',
tfClearFreeze: '0x00200000',
},
}
export const getFlags = (tt?: string) => {
if (!tt) return
const flags = {
...transactionFlags['*'],
...transactionFlags[tt]
}
return flags
}
export function combineFlags(flags?: string[]): string | undefined {
if (!flags) return
const num = flags.reduce((cumm, curr) => cumm | BigInt(curr), BigInt(0))
return num.toString()
}
export function extractFlags(transactionType: string, flags?: string | number,): SelectOption[] {
const flagsObj = getFlags(transactionType)
if (!flags || !flagsObj) return []
const extracted = Object.entries(flagsObj).reduce((cumm, [label, value]) => {
return (BigInt(flags) & BigInt(value)) ? cumm.concat({ label, value }) : cumm
}, [] as SelectOption[])
return extracted
}

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
export * from './templates'
export * from './templates'

Some files were not shown because too many files have changed in this diff Show More