Compare commits
200 Commits
fix/change
...
fix/dest-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae9c7468f | ||
|
|
fb9814ec76 | ||
|
|
d459b2ee92 | ||
|
|
6ee1a09aaa | ||
|
|
dd2228fb35 | ||
|
|
ca52a5e064 | ||
|
|
df0f8abe62 | ||
|
|
a6c4db1951 | ||
|
|
1c91003164 | ||
|
|
66be0efbbd | ||
|
|
9ab64ec062 | ||
|
|
e77a5e234f | ||
|
|
d2f618512a | ||
|
|
f5063de2c9 | ||
|
|
1ee8dcb536 | ||
|
|
7f6f9c11db | ||
|
|
b2b7059774 | ||
|
|
41ba096ef9 | ||
|
|
8b72086c04 | ||
|
|
895b34cc68 | ||
|
|
b9da659f83 | ||
|
|
3897f2d823 | ||
|
|
6a3ff3e1d7 | ||
|
|
bf792f1495 | ||
|
|
df3210a663 | ||
|
|
bad7730c32 | ||
|
|
adb6a78549 | ||
|
|
8cc27f20c3 | ||
|
|
8e49a3f5f1 | ||
|
|
3179757469 | ||
|
|
554cfb3db9 | ||
|
|
637a066f69 | ||
|
|
c9a852e9be | ||
|
|
307a5407eb | ||
|
|
faa28845c8 | ||
|
|
168d11d48e | ||
|
|
60f2bb558c | ||
|
|
fdf33b9f45 | ||
|
|
d05180d148 | ||
|
|
bfaa6be17d | ||
|
|
9e368dec84 | ||
|
|
25eec6980f | ||
|
|
8e2f20c5ac | ||
|
|
a3d094e873 | ||
|
|
ef70bfb13a | ||
|
|
c26c7c13d1 | ||
|
|
fc461ddd0d | ||
|
|
c7001f6089 | ||
|
|
243cbfec08 | ||
|
|
1295e7fa41 | ||
|
|
793623d216 | ||
|
|
0cde0eb240 | ||
|
|
e2acb48e03 | ||
|
|
a4373bb970 | ||
|
|
cfb791092a | ||
|
|
3fcbac5ed9 | ||
|
|
c40b272ce8 | ||
|
|
860ff66a8a | ||
|
|
f4f700bea1 | ||
|
|
789bc00ac3 | ||
|
|
6a0aabdeda | ||
|
|
175b6266e8 | ||
|
|
621482e2ee | ||
|
|
e55f48bc83 | ||
|
|
3e9e26a46a | ||
|
|
f0e730bb9b | ||
|
|
6ce4828fc6 | ||
|
|
bb0a246ae5 | ||
|
|
3af2bad536 | ||
|
|
4f1b877db0 | ||
|
|
0289d64f5e | ||
|
|
868a0bcf78 | ||
|
|
aab2476a05 | ||
|
|
cb25986d72 | ||
|
|
309ad57173 | ||
|
|
53afb1d3d1 | ||
|
|
31ff7c0e28 | ||
|
|
dfa35df465 | ||
|
|
f163b052e1 | ||
|
|
25c5b9c015 | ||
|
|
407e3946ce | ||
|
|
dc5b0d71eb | ||
|
|
3fd6c3f50e | ||
|
|
ec8bfc5eee | ||
|
|
b4a0bcb90d | ||
|
|
2c729e2aa4 | ||
|
|
1cb2542170 | ||
|
|
00b309df34 | ||
|
|
a6fc730de6 | ||
|
|
2245c5a221 | ||
|
|
60c33661ad | ||
|
|
ea21c85038 | ||
|
|
5478f43609 | ||
|
|
a9b64abb85 | ||
|
|
c6ced424d8 | ||
|
|
3a1159cffc | ||
|
|
3136de1bd1 | ||
|
|
67ffd3f1b4 | ||
|
|
8508cb69c4 | ||
|
|
89217d2633 | ||
|
|
ba1b64391c | ||
|
|
098d919a77 | ||
|
|
b2af37ab4b | ||
|
|
dcb7e94e86 | ||
|
|
67848b3d8d | ||
|
|
31a86263a1 | ||
|
|
4d0025afc1 | ||
|
|
f85bd2398d | ||
|
|
a2a6596cc5 | ||
|
|
37208ce97e | ||
|
|
bf4042926d | ||
|
|
3ccc1c16ac | ||
|
|
135f0c91a1 | ||
|
|
8f5786e242 | ||
|
|
810eb4ca27 | ||
|
|
e6574f9f12 | ||
|
|
1a6726fabf | ||
|
|
89f8671217 | ||
|
|
fb5259221b | ||
|
|
fd17f59616 | ||
|
|
91bbc7ea61 | ||
|
|
783d832c6d | ||
|
|
698ca376e7 | ||
|
|
bfd9e21ab8 | ||
|
|
e46411f245 | ||
|
|
08447c6b29 | ||
|
|
9216cc6bf7 | ||
|
|
5108b08e39 | ||
|
|
6c46a4f809 | ||
|
|
0ea88f0d32 | ||
|
|
4c2e1f36f3 | ||
|
|
fa5315fc0e | ||
|
|
eda8b1550c | ||
|
|
742b11374f | ||
|
|
d16e83dcfa | ||
|
|
155aa57784 | ||
|
|
b88b6da7d9 | ||
|
|
fa13f7e282 | ||
|
|
f1a43ef758 | ||
|
|
4217813fd7 | ||
|
|
c588f7b1f3 | ||
|
|
985e8ee820 | ||
|
|
8832e76a0a | ||
|
|
9777f1dbd1 | ||
|
|
213d468aab | ||
|
|
46becb0e7b | ||
|
|
fad6bd100a | ||
|
|
5a11f83fea | ||
|
|
525338abf7 | ||
|
|
ea977816a4 | ||
|
|
0ee599a2b6 | ||
|
|
02c59f8d79 | ||
|
|
3d5b77e60a | ||
|
|
92a167d47a | ||
|
|
d41e263942 | ||
|
|
bd1226fe90 | ||
|
|
57403e42dd | ||
|
|
2b42a96c4a | ||
|
|
80d6bb691d | ||
|
|
c7e4cd7c92 | ||
|
|
4a22861860 | ||
|
|
b09d029931 | ||
|
|
b2dc49754f | ||
|
|
6f636645f7 | ||
|
|
377c963c7a | ||
|
|
ae038f17ff | ||
|
|
0d8f2c31e7 | ||
|
|
da9986eb66 | ||
|
|
a21350770e | ||
|
|
49dfd43220 | ||
|
|
4472957f5c | ||
|
|
ca46707bb5 | ||
|
|
704ebe4b92 | ||
|
|
9a6ef2c393 | ||
|
|
56203ce9c6 | ||
|
|
933bdb5968 | ||
|
|
864711697b | ||
|
|
e5eaf09721 | ||
|
|
d0dde56c67 | ||
|
|
45c6927e72 | ||
|
|
6014b6e79f | ||
|
|
04a99227df | ||
|
|
0965a1e898 | ||
|
|
32445dbebf | ||
|
|
1a1d4901aa | ||
|
|
8b646c56dc | ||
|
|
ac38bbc72c | ||
|
|
bf1182351a | ||
|
|
55e48a943b | ||
|
|
faf417be69 | ||
|
|
c2eb57211f | ||
|
|
0e97df3c8e | ||
|
|
5dd0dfdc18 | ||
|
|
ef48bac8f6 | ||
|
|
3a3d984098 | ||
|
|
2300c201f8 | ||
|
|
329dc4a355 | ||
|
|
cd6a5b23d4 | ||
|
|
4dd7cbe2ca | ||
|
|
260de7c838 |
@@ -1,4 +1,5 @@
|
|||||||
NEXTAUTH_URL=https://example.com
|
NEXTAUTH_URL=https://example.com
|
||||||
|
NEXTAUTH_SECRET="1234"
|
||||||
GITHUB_SECRET=""
|
GITHUB_SECRET=""
|
||||||
GITHUB_ID=""
|
GITHUB_ID=""
|
||||||
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
|
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,3 +32,4 @@ yarn-error.log*
|
|||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# XRPL Hooks IDE
|
# XRPL Hooks Builder
|
||||||
|
|
||||||
This is the repository for XRPL Hooks IDE. This project is built with Next.JS
|
https://hooks-builder.xrpl.org/
|
||||||
|
|
||||||
|
This is the repository for XRPL Hooks Builder. This project is built with Next.JS
|
||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
@@ -106,3 +108,5 @@ 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.
|
- [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!
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import transactionsData from "../content/transactions.json";
|
|||||||
import { SetHookDialog } from "./SetHookDialog";
|
import { SetHookDialog } from "./SetHookDialog";
|
||||||
import { addFunds } from "../state/actions/addFaucetAccount";
|
import { addFunds } from "../state/actions/addFaucetAccount";
|
||||||
import { deleteHook } from "../state/actions/deployHook";
|
import { deleteHook } from "../state/actions/deployHook";
|
||||||
|
import { capitalize } from "../utils/helpers";
|
||||||
|
|
||||||
export const AccountDialog = ({
|
export const AccountDialog = ({
|
||||||
activeAccountAddress,
|
activeAccountAddress,
|
||||||
@@ -42,12 +43,12 @@ export const AccountDialog = ({
|
|||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state);
|
||||||
const [showSecret, setShowSecret] = useState(false);
|
const [showSecret, setShowSecret] = useState(false);
|
||||||
const activeAccount = snap.accounts.find(
|
const activeAccount = snap.accounts.find(
|
||||||
(account) => account.address === activeAccountAddress
|
account => account.address === activeAccountAddress
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={Boolean(activeAccountAddress)}
|
open={Boolean(activeAccountAddress)}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={open => {
|
||||||
setShowSecret(false);
|
setShowSecret(false);
|
||||||
!open && setActiveAccountAddress(null);
|
!open && setActiveAccountAddress(null);
|
||||||
}}
|
}}
|
||||||
@@ -99,7 +100,7 @@ export const AccountDialog = ({
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const index = state.accounts.findIndex(
|
const index = state.accounts.findIndex(
|
||||||
(acc) => acc.address === activeAccount?.address
|
acc => acc.address === activeAccount?.address
|
||||||
);
|
);
|
||||||
state.accounts.splice(index, 1);
|
state.accounts.splice(index, 1);
|
||||||
}}
|
}}
|
||||||
@@ -116,9 +117,16 @@ export const AccountDialog = ({
|
|||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
fontFamily: "$monospace",
|
fontFamily: "$monospace",
|
||||||
|
a: { "&:hover": { textDecoration: "underline" } },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeAccount?.address}
|
<a
|
||||||
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{activeAccount?.address}
|
||||||
|
</a>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
|
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
|
||||||
@@ -158,7 +166,7 @@ export const AccountDialog = ({
|
|||||||
}}
|
}}
|
||||||
ghost
|
ghost
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => setShowSecret((curr) => !curr)}
|
onClick={() => setShowSecret(curr => !curr)}
|
||||||
>
|
>
|
||||||
{showSecret ? "Hide" : "Show"}
|
{showSecret ? "Hide" : "Show"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -215,7 +223,11 @@ export const AccountDialog = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ marginLeft: "auto" }}>
|
<Flex
|
||||||
|
css={{
|
||||||
|
marginLeft: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -237,10 +249,22 @@ export const AccountDialog = ({
|
|||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
fontFamily: "$monospace",
|
fontFamily: "$monospace",
|
||||||
|
a: { "&:hover": { textDecoration: "underline" } },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeAccount && activeAccount.hooks.length > 0
|
{activeAccount && activeAccount.hooks.length > 0
|
||||||
? activeAccount.hooks.map((i) => truncate(i, 12)).join(",")
|
? activeAccount.hooks.map(i => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={i}
|
||||||
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{truncate(i, 12)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})
|
||||||
: "–"}
|
: "–"}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -278,7 +302,7 @@ interface AccountProps {
|
|||||||
showHookStats?: boolean;
|
showHookStats?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Accounts: FC<AccountProps> = (props) => {
|
const Accounts: FC<AccountProps> = props => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state);
|
||||||
const [activeAccountAddress, setActiveAccountAddress] = useState<
|
const [activeAccountAddress, setActiveAccountAddress] = useState<
|
||||||
string | null
|
string | null
|
||||||
@@ -286,7 +310,7 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAccInfo = async () => {
|
const fetchAccInfo = async () => {
|
||||||
if (snap.clientStatus === "online") {
|
if (snap.clientStatus === "online") {
|
||||||
const requests = snap.accounts.map((acc) =>
|
const requests = snap.accounts.map(acc =>
|
||||||
snap.client?.send({
|
snap.client?.send({
|
||||||
id: `hooks-builder-req-info-${acc.address}`,
|
id: `hooks-builder-req-info-${acc.address}`,
|
||||||
command: "account_info",
|
command: "account_info",
|
||||||
@@ -299,14 +323,26 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
const balance = res?.account_data?.Balance as string;
|
const balance = res?.account_data?.Balance as string;
|
||||||
const sequence = res?.account_data?.Sequence as number;
|
const sequence = res?.account_data?.Sequence as number;
|
||||||
const accountToUpdate = state.accounts.find(
|
const accountToUpdate = state.accounts.find(
|
||||||
(acc) => acc.address === address
|
acc => acc.address === address
|
||||||
);
|
);
|
||||||
if (accountToUpdate) {
|
if (accountToUpdate) {
|
||||||
accountToUpdate.xrp = balance;
|
accountToUpdate.xrp = balance;
|
||||||
accountToUpdate.sequence = sequence;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const objectRequests = snap.accounts.map((acc) => {
|
const objectRequests = snap.accounts.map(acc => {
|
||||||
return snap.client?.send({
|
return snap.client?.send({
|
||||||
id: `hooks-builder-req-objects-${acc.address}`,
|
id: `hooks-builder-req-objects-${acc.address}`,
|
||||||
command: "account_objects",
|
command: "account_objects",
|
||||||
@@ -317,7 +353,7 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
objectResponses.forEach((res: any) => {
|
objectResponses.forEach((res: any) => {
|
||||||
const address = res?.account as string;
|
const address = res?.account as string;
|
||||||
const accountToUpdate = state.accounts.find(
|
const accountToUpdate = state.accounts.find(
|
||||||
(acc) => acc.address === address
|
acc => acc.address === address
|
||||||
);
|
);
|
||||||
if (accountToUpdate) {
|
if (accountToUpdate) {
|
||||||
accountToUpdate.hooks =
|
accountToUpdate.hooks =
|
||||||
@@ -343,7 +379,7 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [snap.accounts, snap.clientStatus]);
|
}, [snap.accounts.length, snap.clientStatus]);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as="div"
|
as="div"
|
||||||
@@ -381,9 +417,7 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
|
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
|
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
|
||||||
<Button ghost size="sm" onClick={() => addFaucetAccount(true)}>
|
<ImportAccountDialog type="create" />
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
<ImportAccountDialog />
|
<ImportAccountDialog />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -400,7 +434,7 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{snap.accounts.map((account) => (
|
{snap.accounts.map(account => (
|
||||||
<Flex
|
<Flex
|
||||||
column
|
column
|
||||||
key={account.address + account.name}
|
key={account.address + account.name}
|
||||||
@@ -431,28 +465,33 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{account.address} (
|
{account.address}{" "}
|
||||||
{Dinero({
|
{!account?.error ? (
|
||||||
amount: Number(account?.xrp || "0"),
|
`(${Dinero({
|
||||||
precision: 6,
|
amount: Number(account?.xrp || "0"),
|
||||||
})
|
precision: 6,
|
||||||
.toUnit()
|
})
|
||||||
.toLocaleString(undefined, {
|
.toUnit()
|
||||||
style: "currency",
|
.toLocaleString(undefined, {
|
||||||
currency: "XRP",
|
style: "currency",
|
||||||
currencyDisplay: "name",
|
currency: "XRP",
|
||||||
})}
|
currencyDisplay: "name",
|
||||||
)
|
})})`
|
||||||
|
) : (
|
||||||
|
<Box css={{ color: "$red11" }}>
|
||||||
|
(Account not found, request funds to activate account)
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{!props.hideDeployBtn && (
|
{!props.hideDeployBtn && (
|
||||||
<div
|
<div
|
||||||
className="hook-deploy-button"
|
className="hook-deploy-button"
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SetHookDialog account={account} />
|
<SetHookDialog accountAddress={account.address} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -474,31 +513,71 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transactionsOptions = transactionsData.map((tx) => ({
|
export const transactionsOptions = transactionsData.map(tx => ({
|
||||||
value: tx.TransactionType,
|
value: tx.TransactionType,
|
||||||
label: tx.TransactionType,
|
label: tx.TransactionType,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ImportAccountDialog = () => {
|
const ImportAccountDialog = ({
|
||||||
const [value, setValue] = useState("");
|
type = "import",
|
||||||
|
}: {
|
||||||
|
type?: "import" | "create";
|
||||||
|
}) => {
|
||||||
|
const [secret, setSecret] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
|
||||||
|
const btnText = type === "import" ? "Import" : "Create";
|
||||||
|
const title = type === "import" ? "Import Account" : "Create Account";
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (type === "create") {
|
||||||
|
const value = capitalize(name);
|
||||||
|
await addFaucetAccount(value, true);
|
||||||
|
setName("");
|
||||||
|
setSecret("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importAccount(secret, name);
|
||||||
|
setName("");
|
||||||
|
setSecret("");
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button ghost size="sm">
|
<Button ghost size="sm">
|
||||||
Import
|
{btnText}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent aria-describedby={undefined}>
|
||||||
<DialogTitle>Import account</DialogTitle>
|
<DialogTitle css={{ mb: "$4" }}>{title}</DialogTitle>
|
||||||
<DialogDescription>
|
<Flex column>
|
||||||
<Label>Add account secret</Label>
|
<Box css={{ mb: "$2" }}>
|
||||||
<Input
|
<Label>
|
||||||
name="secret"
|
Account name <Text muted>(optional)</Text>
|
||||||
type="password"
|
</Label>
|
||||||
value={value}
|
<Input
|
||||||
onChange={(e) => setValue(e.target.value)}
|
name="name"
|
||||||
/>
|
type="text"
|
||||||
</DialogDescription>
|
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>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
@@ -511,14 +590,8 @@ const ImportAccountDialog = () => {
|
|||||||
<Button outline>Cancel</Button>
|
<Button outline>Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button type="submit" variant="primary" onClick={handleSubmit}>
|
||||||
variant="primary"
|
{title}
|
||||||
onClick={() => {
|
|
||||||
importAccount(value);
|
|
||||||
setValue("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Import account
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
135
components/ContextMenu/index.tsx
Normal file
135
components/ContextMenu/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
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;
|
||||||
107
components/ContextMenu/primitive.tsx
Normal file
107
components/ContextMenu/primitive.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||||
|
import { styled } from "../../stitches.config";
|
||||||
|
import {
|
||||||
|
slideDownAndFade,
|
||||||
|
slideLeftAndFade,
|
||||||
|
slideRightAndFade,
|
||||||
|
slideUpAndFade,
|
||||||
|
} from "../../styles/keyframes";
|
||||||
|
|
||||||
|
const StyledContent = styled(ContextMenuPrimitive.Content, {
|
||||||
|
minWidth: 140,
|
||||||
|
backgroundColor: "$backgroundOverlay",
|
||||||
|
borderRadius: 6,
|
||||||
|
overflow: "hidden",
|
||||||
|
padding: "5px",
|
||||||
|
boxShadow:
|
||||||
|
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
|
||||||
|
"@media (prefers-reduced-motion: no-preference)": {
|
||||||
|
animationDuration: "400ms",
|
||||||
|
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
|
willChange: "transform, opacity",
|
||||||
|
'&[data-state="open"]': {
|
||||||
|
'&[data-side="top"]': { animationName: slideDownAndFade },
|
||||||
|
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
||||||
|
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
||||||
|
'&[data-side="left"]': { animationName: slideRightAndFade },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
".dark &": {
|
||||||
|
boxShadow:
|
||||||
|
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemStyles = {
|
||||||
|
all: "unset",
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: "$text",
|
||||||
|
borderRadius: 3,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: 28,
|
||||||
|
padding: "0 7px",
|
||||||
|
position: "relative",
|
||||||
|
paddingLeft: 10,
|
||||||
|
userSelect: "none",
|
||||||
|
|
||||||
|
"&[data-disabled]": {
|
||||||
|
color: "$textMuted",
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
|
|
||||||
|
"&:focus": {
|
||||||
|
backgroundColor: "$purple9",
|
||||||
|
color: "$white",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles });
|
||||||
|
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
|
||||||
|
...itemStyles,
|
||||||
|
});
|
||||||
|
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
|
||||||
|
...itemStyles,
|
||||||
|
});
|
||||||
|
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
|
||||||
|
'&[data-state="open"]': {
|
||||||
|
backgroundColor: "$purple9",
|
||||||
|
color: "$purple9",
|
||||||
|
},
|
||||||
|
...itemStyles,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledLabel = styled(ContextMenuPrimitive.Label, {
|
||||||
|
paddingLeft: 10,
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: "25px",
|
||||||
|
color: "$text",
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: "$backgroundAlt",
|
||||||
|
margin: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, {
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
width: 25,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ContextMenuRoot = ContextMenuPrimitive.Root;
|
||||||
|
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||||
|
export const ContextMenuContent = StyledContent;
|
||||||
|
export const ContextMenuItem = StyledItem;
|
||||||
|
export const ContextMenuCheckboxItem = StyledCheckboxItem;
|
||||||
|
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||||
|
export const ContextMenuRadioItem = StyledRadioItem;
|
||||||
|
export const ContextMenuItemIndicator = StyledItemIndicator;
|
||||||
|
export const ContextMenuTriggerItem = StyledTriggerItem;
|
||||||
|
export const ContextMenuLabel = StyledLabel;
|
||||||
|
export const ContextMenuSeparator = StyledSeparator;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import ReconnectingWebSocket, { CloseEvent } from "reconnecting-websocket";
|
||||||
import { proxy, ref, useSnapshot } from "valtio";
|
import { proxy, ref, useSnapshot } from "valtio";
|
||||||
|
import { subscribeKey } from "valtio/utils";
|
||||||
import { Select } from ".";
|
import { Select } from ".";
|
||||||
import state, { ILog, transactionsState } from "../state";
|
import state, { ILog, transactionsState } from "../state";
|
||||||
import { extractJSON } from "../utils/json";
|
import { extractJSON } from "../utils/json";
|
||||||
@@ -15,7 +17,7 @@ export interface IStreamState {
|
|||||||
status: "idle" | "opened" | "closed";
|
status: "idle" | "opened" | "closed";
|
||||||
statusChangeTimestamp?: number;
|
statusChangeTimestamp?: number;
|
||||||
logs: ILog[];
|
logs: ILog[];
|
||||||
socket?: WebSocket;
|
socket?: ReconnectingWebSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const streamState = proxy<IStreamState>({
|
export const streamState = proxy<IStreamState>({
|
||||||
@@ -24,12 +26,85 @@ export const streamState = proxy<IStreamState>({
|
|||||||
logs: [] as ILog[],
|
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 DebugStream = () => {
|
const DebugStream = () => {
|
||||||
const { selectedAccount, logs, socket } = useSnapshot(streamState);
|
const { selectedAccount, logs } = useSnapshot(streamState);
|
||||||
const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
|
const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
|
||||||
const { accounts } = useSnapshot(state);
|
const { accounts } = useSnapshot(state);
|
||||||
|
|
||||||
const accountOptions = accounts.map(acc => ({
|
const accountOptions = accounts.map((acc) => ({
|
||||||
label: acc.name,
|
label: acc.name,
|
||||||
value: acc.address,
|
value: acc.address,
|
||||||
}));
|
}));
|
||||||
@@ -42,117 +117,21 @@ const DebugStream = () => {
|
|||||||
options={accountOptions}
|
options={accountOptions}
|
||||||
hideSelectedOptions
|
hideSelectedOptions
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={acc => (streamState.selectedAccount = acc as any)}
|
onChange={(acc) => {
|
||||||
|
streamState.socket?.close(
|
||||||
|
4999,
|
||||||
|
"Old connection closed because user switched account"
|
||||||
|
);
|
||||||
|
streamState.selectedAccount = acc as any;
|
||||||
|
}}
|
||||||
css={{ width: "100%" }}
|
css={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const onMount = useCallback(async () => {
|
|
||||||
// deliberately using `proxy` values and not the `useSnapshot` ones to have no dep list
|
|
||||||
const acc = streamState.selectedAccount;
|
|
||||||
const status = streamState.status;
|
|
||||||
|
|
||||||
if (status === "opened" && acc) {
|
|
||||||
// fetch the missing ones
|
|
||||||
try {
|
|
||||||
const url = `https://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/recent/${acc?.value}`;
|
|
||||||
|
|
||||||
// TODO Remove after api sets cors properly
|
|
||||||
const res = await fetch("/api/proxy", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ url }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) return;
|
|
||||||
|
|
||||||
const body = await res.json();
|
|
||||||
|
|
||||||
if (!body?.logs) return;
|
|
||||||
|
|
||||||
const start = streamState.statusChangeTimestamp || 0;
|
|
||||||
streamState.logs = [];
|
|
||||||
pushLog(`Debug stream opened for account ${acc.value}`, {
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
|
|
||||||
const logs = Object.entries(body.logs).filter(([tm]) => +tm >= start);
|
|
||||||
|
|
||||||
logs.forEach(([tm, log]) => pushLog(log));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onMount();
|
|
||||||
}, [onMount]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const account = selectedAccount?.value;
|
|
||||||
const socket = streamState.socket;
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const onOpen = () => {
|
|
||||||
streamState.logs = [];
|
|
||||||
streamState.status = "opened";
|
|
||||||
streamState.statusChangeTimestamp = Date.now();
|
|
||||||
pushLog(`Debug stream opened for account ${account}`, {
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onError = () => {
|
|
||||||
pushLog("Something went wrong! Check your connection and try again.", {
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onClose = (e: CloseEvent) => {
|
|
||||||
pushLog(`Connection was closed. [code: ${e.code}]`, {
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
streamState.selectedAccount = null;
|
|
||||||
streamState.status = "closed";
|
|
||||||
streamState.statusChangeTimestamp = Date.now();
|
|
||||||
};
|
|
||||||
const onMessage = (event: any) => {
|
|
||||||
pushLog(event.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}, [selectedAccount?.value, socket]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const account = transactionsState.transactions.find(
|
const account = transactionsState.transactions.find(
|
||||||
tx => tx.header === activeTxTab
|
(tx) => tx.header === activeTxTab
|
||||||
)?.state.selectedAccount;
|
)?.state.selectedAccount;
|
||||||
|
|
||||||
if (account && account.value !== streamState.selectedAccount?.value)
|
if (account && account.value !== streamState.selectedAccount?.value)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useSnapshot, ref } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import Editor, { loader } from "@monaco-editor/react";
|
|
||||||
import type monaco from "monaco-editor";
|
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import NextLink from "next/link";
|
import NextLink from "next/link";
|
||||||
@@ -10,31 +9,36 @@ import filesize from "filesize";
|
|||||||
|
|
||||||
import Box from "./Box";
|
import Box from "./Box";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import dark from "../theme/editor/amy.json";
|
|
||||||
import light from "../theme/editor/xcode_default.json";
|
|
||||||
import state from "../state";
|
import state from "../state";
|
||||||
import wat from "../utils/wat-highlight";
|
import wat from "../utils/wat-highlight";
|
||||||
|
|
||||||
import EditorNavigation from "./EditorNavigation";
|
import EditorNavigation from "./EditorNavigation";
|
||||||
import { Button, Text, Link, Flex } from ".";
|
import { Button, Text, Link, Flex, Tabs, Tab } from ".";
|
||||||
|
import Monaco from "./Monaco";
|
||||||
loader.config({
|
|
||||||
paths: {
|
|
||||||
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
|
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
|
||||||
|
|
||||||
const DeployEditor = () => {
|
const DeployEditor = () => {
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
|
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
const activeFile = snap.files[snap.active];
|
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 compiledSize = activeFile?.compiledContent?.byteLength || 0;
|
||||||
const color =
|
const color =
|
||||||
compiledSize > FILESIZE_BREAKPOINTS[1]
|
compiledSize > FILESIZE_BREAKPOINTS[1]
|
||||||
@@ -43,6 +47,10 @@ const DeployEditor = () => {
|
|||||||
? "$warning"
|
? "$warning"
|
||||||
: "$success";
|
: "$success";
|
||||||
|
|
||||||
|
const isContentChanged =
|
||||||
|
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
|
||||||
|
// const hasDeployErrors = activeFile && activeFile.containsErrors;
|
||||||
|
|
||||||
const CompiledStatView = activeFile && (
|
const CompiledStatView = activeFile && (
|
||||||
<Flex
|
<Flex
|
||||||
column
|
column
|
||||||
@@ -60,15 +68,30 @@ const DeployEditor = () => {
|
|||||||
{activeFile?.lastCompiled && (
|
{activeFile?.lastCompiled && (
|
||||||
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
|
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeFile.compiledContent?.byteLength && (
|
{activeFile.compiledContent?.byteLength && (
|
||||||
<Text css={{ ml: "$2", color }}>
|
<Text css={{ ml: "$2", color }}>
|
||||||
({filesize(activeFile.compiledContent.byteLength)})
|
({filesize(activeFile.compiledContent.byteLength)})
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</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)}>
|
<Button variant="link" onClick={() => setShowContent(true)}>
|
||||||
View as WAT-file
|
View as WAT-file
|
||||||
</Button>
|
</Button>
|
||||||
|
{isContentChanged && (
|
||||||
|
<Text warning>
|
||||||
|
File contents were changed after last compile, compile again to
|
||||||
|
incorporate your latest changes in the build.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
const NoContentView = !snap.loading && router.isReady && (
|
const NoContentView = !snap.loading && router.isReady && (
|
||||||
@@ -87,8 +110,9 @@ const DeployEditor = () => {
|
|||||||
</NextLink>
|
</NextLink>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isContent =
|
const isContent =
|
||||||
snap.files?.filter((file) => file.compiledWatContent).length > 0 &&
|
snap.files?.filter(file => file.compiledWatContent).length > 0 &&
|
||||||
router.isReady;
|
router.isReady;
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -101,7 +125,7 @@ const DeployEditor = () => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditorNavigation showWat />
|
<EditorNavigation renderNav={renderNav} />
|
||||||
<Container
|
<Container
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -115,32 +139,38 @@ const DeployEditor = () => {
|
|||||||
) : !showContent ? (
|
) : !showContent ? (
|
||||||
CompiledStatView
|
CompiledStatView
|
||||||
) : (
|
) : (
|
||||||
<Editor
|
<Monaco
|
||||||
className="hooks-editor"
|
className="hooks-editor"
|
||||||
defaultLanguage={"wat"}
|
defaultLanguage={"wat"}
|
||||||
language={"wat"}
|
language={"wat"}
|
||||||
path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
|
path={`file://tmp/c/${activeFile?.name}.wat`}
|
||||||
value={snap.files?.[snap.active]?.compiledWatContent || ""}
|
value={activeFile?.compiledWatContent || ""}
|
||||||
beforeMount={(monaco) => {
|
beforeMount={monaco => {
|
||||||
monaco.languages.register({ id: "wat" });
|
monaco.languages.register({ id: "wat" });
|
||||||
monaco.languages.setLanguageConfiguration("wat", wat.config);
|
monaco.languages.setLanguageConfiguration("wat", wat.config);
|
||||||
monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
|
monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
|
||||||
if (!state.editorCtx) {
|
|
||||||
state.editorCtx = ref(monaco.editor);
|
|
||||||
// @ts-expect-error
|
|
||||||
monaco.editor.defineTheme("dark", dark);
|
|
||||||
// @ts-expect-error
|
|
||||||
monaco.editor.defineTheme("light", light);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onMount={(editor, monaco) => {
|
onMount={editor => {
|
||||||
editorRef.current = editor;
|
|
||||||
editor.updateOptions({
|
editor.updateOptions({
|
||||||
glyphMargin: true,
|
glyphMargin: true,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
theme={theme === "dark" ? "dark" : "light"}
|
theme={theme === "dark" ? "dark" : "light"}
|
||||||
|
overlay={
|
||||||
|
<Flex
|
||||||
|
css={{
|
||||||
|
m: "$1",
|
||||||
|
ml: "auto",
|
||||||
|
fontSize: "$sm",
|
||||||
|
color: "$textMuted",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link onClick={() => setShowContent(false)}>
|
||||||
|
Exit editor mode
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -15,7 +15,7 @@ const contentShow = keyframes({
|
|||||||
"100%": { opacity: 1 },
|
"100%": { opacity: 1 },
|
||||||
});
|
});
|
||||||
const StyledOverlay = styled(DialogPrimitive.Overlay, {
|
const StyledOverlay = styled(DialogPrimitive.Overlay, {
|
||||||
zIndex: 9999,
|
zIndex: 10000,
|
||||||
backgroundColor: blackA.blackA9,
|
backgroundColor: blackA.blackA9,
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
@@ -40,6 +40,7 @@ const StyledContent = styled(DialogPrimitive.Content, {
|
|||||||
color: "$mauve12",
|
color: "$mauve12",
|
||||||
borderRadius: "$md",
|
borderRadius: "$md",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
mb: "15%",
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
|
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
|
||||||
width: "90vw",
|
width: "90vw",
|
||||||
|
|||||||
@@ -1,27 +1,7 @@
|
|||||||
import { keyframes } from "@stitches/react";
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from "../stitches.config";
|
||||||
|
import { slideDownAndFade, slideLeftAndFade, slideRightAndFade, slideUpAndFade } from '../styles/keyframes';
|
||||||
const slideUpAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateY(2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const slideRightAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateX(-2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const slideDownAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateY(-2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const slideLeftAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateX(2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const StyledContent = styled(DropdownMenuPrimitive.Content, {
|
const StyledContent = styled(DropdownMenuPrimitive.Content, {
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Plus,
|
|
||||||
Share,
|
Share,
|
||||||
DownloadSimple,
|
DownloadSimple,
|
||||||
Gear,
|
Gear,
|
||||||
@@ -28,7 +32,6 @@ import { useSnapshot } from "valtio";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createNewFile,
|
|
||||||
syncToGist,
|
syncToGist,
|
||||||
updateEditorSettings,
|
updateEditorSettings,
|
||||||
downloadAsZip,
|
downloadAsZip,
|
||||||
@@ -48,36 +51,23 @@ import {
|
|||||||
import Flex from "./Flex";
|
import Flex from "./Flex";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { Input, Label } from "./Input";
|
import { Input, Label } from "./Input";
|
||||||
import Text from "./Text";
|
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
import { styled } from "../stitches.config";
|
|
||||||
import { showAlert } from "../state/actions/showAlert";
|
import { showAlert } from "../state/actions/showAlert";
|
||||||
|
|
||||||
const ErrorText = styled(Text, {
|
|
||||||
color: "$error",
|
|
||||||
mt: "$1",
|
|
||||||
display: "block",
|
|
||||||
});
|
|
||||||
|
|
||||||
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state);
|
||||||
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
|
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
|
||||||
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
|
|
||||||
const [newfileError, setNewfileError] = useState<string | null>(null);
|
|
||||||
const [filename, setFilename] = useState("");
|
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const [popup, setPopUp] = useState(false);
|
const [popup, setPopUp] = useState(false);
|
||||||
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
|
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session && session.user && popup) {
|
if (session && session.user && popup) {
|
||||||
setPopUp(false);
|
setPopUp(false);
|
||||||
}
|
}
|
||||||
}, [session, popup]);
|
}, [session, popup]);
|
||||||
|
|
||||||
// when filename changes, reset error
|
|
||||||
useEffect(() => {
|
|
||||||
setNewfileError(null);
|
|
||||||
}, [filename, setNewfileError]);
|
|
||||||
|
|
||||||
const showNewGistAlert = () => {
|
const showNewGistAlert = () => {
|
||||||
showAlert("Are you sure?", {
|
showAlert("Are you sure?", {
|
||||||
@@ -95,177 +85,55 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateFilename = useCallback(
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
(filename: string): { error: string | null } => {
|
const containerRef = useRef<HTMLDivElement>(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 (
|
return (
|
||||||
<Flex css={{ flexShrink: 0, gap: "$0" }}>
|
<Flex css={{ flexShrink: 0, gap: "$0" }}>
|
||||||
<Flex
|
<Flex
|
||||||
|
id="kissa"
|
||||||
|
ref={scrollRef}
|
||||||
css={{
|
css={{
|
||||||
overflowX: "scroll",
|
overflowX: "scroll",
|
||||||
|
overflowY: "hidden",
|
||||||
py: "$3",
|
py: "$3",
|
||||||
|
pb: "$0",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
"&::-webkit-scrollbar": {
|
"&::-webkit-scrollbar": {
|
||||||
height: 0,
|
height: "0.3em",
|
||||||
background: "transparent",
|
background: "rgba(0,0,0,.0)",
|
||||||
|
},
|
||||||
|
"&::-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 }}>
|
<Container css={{ flex: 1 }} ref={containerRef}>
|
||||||
<Stack
|
{renderNav?.()}
|
||||||
css={{
|
|
||||||
gap: "$3",
|
|
||||||
flex: 1,
|
|
||||||
flexWrap: "nowrap",
|
|
||||||
marginBottom: "-1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{files &&
|
|
||||||
files.length > 0 &&
|
|
||||||
files.map((file, index) => {
|
|
||||||
if (!file.compiledContent && showWat) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
outline={
|
|
||||||
showWat ? snap.activeWat !== index : snap.active !== index
|
|
||||||
}
|
|
||||||
onClick={() => (state.active = index)}
|
|
||||||
key={file.name + index}
|
|
||||||
css={{
|
|
||||||
"&:hover": {
|
|
||||||
span: {
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{file.name}
|
|
||||||
{showWat && ".wat"}
|
|
||||||
{!showWat && (
|
|
||||||
<Box
|
|
||||||
as="span"
|
|
||||||
css={{
|
|
||||||
display: "flex",
|
|
||||||
p: "2px",
|
|
||||||
borderRadius: "$full",
|
|
||||||
mr: "-4px",
|
|
||||||
"&:hover": {
|
|
||||||
// boxSizing: "0px 0px 1px",
|
|
||||||
backgroundColor: "$mauve2",
|
|
||||||
color: "$mauve12",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onClick={(ev: React.MouseEvent<HTMLElement>) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
// Remove file from state
|
|
||||||
state.files.splice(index, 1);
|
|
||||||
// Change active file state
|
|
||||||
// If deleted file is behind active tab
|
|
||||||
// we keep the current state otherwise
|
|
||||||
// select previous file on the list
|
|
||||||
state.active =
|
|
||||||
index > snap.active ? snap.active : snap.active - 1;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X size="9px" weight="bold" />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!showWat && (
|
|
||||||
<Dialog
|
|
||||||
open={isNewfileDialogOpen}
|
|
||||||
onOpenChange={setIsNewfileDialogOpen}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
ghost
|
|
||||||
size="sm"
|
|
||||||
css={{ alignItems: "center", px: "$2", mr: "$3" }}
|
|
||||||
>
|
|
||||||
<Plus size="16px" />{" "}
|
|
||||||
{snap.files.length === 0 && "Add new file"}
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle>Create new file</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<Label>Filename</Label>
|
|
||||||
<Input
|
|
||||||
value={filename}
|
|
||||||
onChange={e => setFilename(e.target.value)}
|
|
||||||
onKeyPress={e => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
handleConfirm();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ErrorText>{newfileError}</ErrorText>
|
|
||||||
</DialogDescription>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
css={{
|
|
||||||
marginTop: 25,
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
gap: "$3",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button outline>Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button variant="primary" onClick={handleConfirm}>
|
|
||||||
Create file
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
|
||||||
<X size="20px" />
|
|
||||||
</Box>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { useSnapshot, ref } from "valtio";
|
import { useSnapshot, ref } from "valtio";
|
||||||
import Editor, { loader } from "@monaco-editor/react";
|
|
||||||
import type monaco from "monaco-editor";
|
import type monaco from "monaco-editor";
|
||||||
import { ArrowBendLeftUp } from "phosphor-react";
|
import { ArrowBendLeftUp } from "phosphor-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
@@ -8,9 +7,7 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import Box from "./Box";
|
import Box from "./Box";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import dark from "../theme/editor/amy.json";
|
import { createNewFile, saveFile } from "../state/actions";
|
||||||
import light from "../theme/editor/xcode_default.json";
|
|
||||||
import { saveFile } from "../state/actions";
|
|
||||||
import { apiHeaderFiles } from "../state/constants";
|
import { apiHeaderFiles } from "../state/constants";
|
||||||
import state from "../state";
|
import state from "../state";
|
||||||
|
|
||||||
@@ -22,16 +19,14 @@ import { listen } from "@codingame/monaco-jsonrpc";
|
|||||||
import ReconnectingWebSocket from "reconnecting-websocket";
|
import ReconnectingWebSocket from "reconnecting-websocket";
|
||||||
|
|
||||||
import docs from "../xrpl-hooks-docs/docs";
|
import docs from "../xrpl-hooks-docs/docs";
|
||||||
|
import Monaco from "./Monaco";
|
||||||
loader.config({
|
import { saveAllFiles } from "../state/actions/saveFile";
|
||||||
paths: {
|
import { Tab, Tabs } from "./Tabs";
|
||||||
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
|
import { renameFile } from "../state/actions/createNewFile";
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||||
const currPath = editor.getModel()?.uri.path;
|
const currPath = editor.getModel()?.uri.path;
|
||||||
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
|
if (apiHeaderFiles.find(h => currPath?.endsWith(h))) {
|
||||||
editor.updateOptions({ readOnly: true });
|
editor.updateOptions({ readOnly: true });
|
||||||
} else {
|
} else {
|
||||||
editor.updateOptions({ readOnly: false });
|
editor.updateOptions({ readOnly: false });
|
||||||
@@ -48,7 +43,7 @@ const setMarkers = (monacoE: typeof monaco) => {
|
|||||||
.getModelMarkers({})
|
.getModelMarkers({})
|
||||||
// Filter out the markers that are hooks specific
|
// Filter out the markers that are hooks specific
|
||||||
.filter(
|
.filter(
|
||||||
(marker) =>
|
marker =>
|
||||||
typeof marker?.code === "string" &&
|
typeof marker?.code === "string" &&
|
||||||
// Take only markers that starts with "hooks-"
|
// Take only markers that starts with "hooks-"
|
||||||
marker?.code?.includes("hooks-")
|
marker?.code?.includes("hooks-")
|
||||||
@@ -62,16 +57,16 @@ const setMarkers = (monacoE: typeof monaco) => {
|
|||||||
// Add decoration (aka extra hoverMessages) to markers in the
|
// Add decoration (aka extra hoverMessages) to markers in the
|
||||||
// exact same range (location) where the markers are
|
// exact same range (location) where the markers are
|
||||||
const models = monacoE.editor.getModels();
|
const models = monacoE.editor.getModels();
|
||||||
models.forEach((model) => {
|
models.forEach(model => {
|
||||||
decorations[model.id] = model?.deltaDecorations(
|
decorations[model.id] = model?.deltaDecorations(
|
||||||
decorations?.[model.id] || [],
|
decorations?.[model.id] || [],
|
||||||
markers
|
markers
|
||||||
.filter((marker) =>
|
.filter(marker =>
|
||||||
marker?.resource.path
|
marker?.resource.path
|
||||||
.split("/")
|
.split("/")
|
||||||
.includes(`${state.files?.[state.active]?.name}`)
|
.includes(`${state.files?.[state.active]?.name}`)
|
||||||
)
|
)
|
||||||
.map((marker) => ({
|
.map(marker => ({
|
||||||
range: new monacoE.Range(
|
range: new monacoE.Range(
|
||||||
marker.startLineNumber,
|
marker.startLineNumber,
|
||||||
marker.startColumn,
|
marker.startColumn,
|
||||||
@@ -119,6 +114,34 @@ const HooksEditor = () => {
|
|||||||
setMarkers(monacoRef.current);
|
setMarkers(monacoRef.current);
|
||||||
}
|
}
|
||||||
}, [snap.active]);
|
}, [snap.active]);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
saveAllFiles();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const file = snap.files[snap.active];
|
||||||
|
|
||||||
|
const renderNav = () => (
|
||||||
|
<Tabs
|
||||||
|
label="File"
|
||||||
|
activeIndex={snap.active}
|
||||||
|
onChangeActive={idx => (state.active = idx)}
|
||||||
|
extensionRequired
|
||||||
|
onCreateNewTab={createNewFile}
|
||||||
|
onCloseTab={idx => state.files.splice(idx, 1)}
|
||||||
|
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} />;
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
@@ -131,18 +154,18 @@ const HooksEditor = () => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditorNavigation />
|
<EditorNavigation renderNav={renderNav} />
|
||||||
{snap.files.length > 0 && router.isReady ? (
|
{snap.files.length > 0 && router.isReady ? (
|
||||||
<Editor
|
<Monaco
|
||||||
className="hooks-editor"
|
|
||||||
keepCurrentModel
|
keepCurrentModel
|
||||||
defaultLanguage={snap.files?.[snap.active]?.language}
|
defaultLanguage={file?.language}
|
||||||
language={snap.files?.[snap.active]?.language}
|
language={file?.language}
|
||||||
path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
|
path={`file:///work/c/${file?.name}`}
|
||||||
defaultValue={snap.files?.[snap.active]?.content}
|
defaultValue={file?.content}
|
||||||
beforeMount={(monaco) => {
|
// onChange={val => (state.files[snap.active].content = val)} // Auto save?
|
||||||
|
beforeMount={monaco => {
|
||||||
if (!snap.editorCtx) {
|
if (!snap.editorCtx) {
|
||||||
snap.files.forEach((file) =>
|
snap.files.forEach(file =>
|
||||||
monaco.editor.createModel(
|
monaco.editor.createModel(
|
||||||
file.content,
|
file.content,
|
||||||
file.language,
|
file.language,
|
||||||
@@ -167,13 +190,13 @@ const HooksEditor = () => {
|
|||||||
// listen when the web socket is opened
|
// listen when the web socket is opened
|
||||||
listen({
|
listen({
|
||||||
webSocket: webSocket as WebSocket,
|
webSocket: webSocket as WebSocket,
|
||||||
onConnection: (connection) => {
|
onConnection: connection => {
|
||||||
// create and start the language client
|
// create and start the language client
|
||||||
const languageClient = createLanguageClient(connection);
|
const languageClient = createLanguageClient(connection);
|
||||||
const disposable = languageClient.start();
|
const disposable = languageClient.start();
|
||||||
|
|
||||||
connection.onClose(() => {
|
connection.onClose(() => {
|
||||||
try {
|
try {
|
||||||
// disposable.stop();
|
|
||||||
disposable.dispose();
|
disposable.dispose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("err", err);
|
console.log("err", err);
|
||||||
@@ -183,7 +206,6 @@ const HooksEditor = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// // hook editor to global state
|
|
||||||
// editor.updateOptions({
|
// editor.updateOptions({
|
||||||
// minimap: {
|
// minimap: {
|
||||||
// enabled: false,
|
// enabled: false,
|
||||||
@@ -192,10 +214,6 @@ const HooksEditor = () => {
|
|||||||
// });
|
// });
|
||||||
if (!state.editorCtx) {
|
if (!state.editorCtx) {
|
||||||
state.editorCtx = ref(monaco.editor);
|
state.editorCtx = ref(monaco.editor);
|
||||||
// @ts-expect-error
|
|
||||||
monaco.editor.defineTheme("dark", dark);
|
|
||||||
// @ts-expect-error
|
|
||||||
monaco.editor.defineTheme("light", light);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMount={(editor, monaco) => {
|
onMount={(editor, monaco) => {
|
||||||
@@ -223,13 +241,13 @@ const HooksEditor = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Hacky way to hide Peek menu
|
// Hacky way to hide Peek menu
|
||||||
editor.onContextMenu((e) => {
|
editor.onContextMenu(e => {
|
||||||
const host =
|
const host =
|
||||||
document.querySelector<HTMLElement>(".shadow-root-host");
|
document.querySelector<HTMLElement>(".shadow-root-host");
|
||||||
|
|
||||||
const contextMenuItems =
|
const contextMenuItems =
|
||||||
host?.shadowRoot?.querySelectorAll("li.action-item");
|
host?.shadowRoot?.querySelectorAll("li.action-item");
|
||||||
contextMenuItems?.forEach((k) => {
|
contextMenuItems?.forEach(k => {
|
||||||
// If menu item contains "Peek" lets hide it
|
// If menu item contains "Peek" lets hide it
|
||||||
if (k.querySelector(".action-label")?.textContent === "Peek") {
|
if (k.querySelector(".action-label")?.textContent === "Peek") {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Notepad, Prohibit } from "phosphor-react";
|
import { IconProps, Notepad, Prohibit } from "phosphor-react";
|
||||||
import useStayScrolled from "react-stay-scrolled";
|
import useStayScrolled from "react-stay-scrolled";
|
||||||
import NextLink from "next/link";
|
import NextLink from "next/link";
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ interface ILogBox {
|
|||||||
logs: ILog[];
|
logs: ILog[];
|
||||||
renderNav?: () => ReactNode;
|
renderNav?: () => ReactNode;
|
||||||
enhanced?: boolean;
|
enhanced?: boolean;
|
||||||
|
Icon?: FC<IconProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogBox: FC<ILogBox> = ({
|
const LogBox: FC<ILogBox> = ({
|
||||||
@@ -33,6 +34,7 @@ const LogBox: FC<ILogBox> = ({
|
|||||||
children,
|
children,
|
||||||
renderNav,
|
renderNav,
|
||||||
enhanced,
|
enhanced,
|
||||||
|
Icon = Notepad,
|
||||||
}) => {
|
}) => {
|
||||||
const logRef = useRef<HTMLPreElement>(null);
|
const logRef = useRef<HTMLPreElement>(null);
|
||||||
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
|
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
|
||||||
@@ -82,14 +84,14 @@ const LogBox: FC<ILogBox> = ({
|
|||||||
gap: "$3",
|
gap: "$3",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
|
<Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
align="center"
|
align="center"
|
||||||
css={{
|
// css={{
|
||||||
width: "50%", // TODO make it max without breaking layout!
|
// maxWidth: "100%", // TODO make it max without breaking layout!
|
||||||
}}
|
// }}
|
||||||
>
|
>
|
||||||
{renderNav?.()}
|
{renderNav?.()}
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -162,11 +164,11 @@ export const Log: FC<ILog> = ({
|
|||||||
(str?: string): ReactNode => {
|
(str?: string): ReactNode => {
|
||||||
if (!str || !accounts.length) return null;
|
if (!str || !accounts.length) return null;
|
||||||
|
|
||||||
const pattern = `(${accounts.map((acc) => acc.address).join("|")})`;
|
const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
|
||||||
const res = regexifyString({
|
const res = regexifyString({
|
||||||
pattern: new RegExp(pattern, "gim"),
|
pattern: new RegExp(pattern, "gim"),
|
||||||
decorator: (match, idx) => {
|
decorator: (match, idx) => {
|
||||||
const name = accounts.find((acc) => acc.address === match)?.name;
|
const name = accounts.find(acc => acc.address === match)?.name;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={match + idx}
|
key={match + idx}
|
||||||
@@ -188,13 +190,13 @@ export const Log: FC<ILog> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
let message: ReactNode;
|
let message: ReactNode;
|
||||||
|
|
||||||
if (typeof _message === 'string') {
|
if (typeof _message === "string") {
|
||||||
_message = _message.trim().replace(/\n /gi, "\n");
|
_message = _message.trim().replace(/\n /gi, "\n");
|
||||||
message = enrichAccounts(_message)
|
if (_message) message = enrichAccounts(_message);
|
||||||
}
|
else message = <Text muted>{'""'}</Text>
|
||||||
else {
|
} else {
|
||||||
message = _message
|
message = _message;
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonData = enrichAccounts(_jsonData);
|
const jsonData = enrichAccounts(_jsonData);
|
||||||
|
|||||||
75
components/Monaco.tsx
Normal file
75
components/Monaco.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import Editor, { loader, EditorProps, Monaco } from "@monaco-editor/react";
|
||||||
|
import { CSS } from "@stitches/react";
|
||||||
|
import type monaco from "monaco-editor";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { FC, MutableRefObject, ReactNode } from "react";
|
||||||
|
import { Flex } from ".";
|
||||||
|
import dark from "../theme/editor/amy.json";
|
||||||
|
import light from "../theme/editor/xcode_default.json";
|
||||||
|
|
||||||
|
export type MonacoProps = EditorProps & {
|
||||||
|
id?: string;
|
||||||
|
rootProps?: { css: CSS } & Record<string, any>;
|
||||||
|
overlay?: ReactNode;
|
||||||
|
editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor>;
|
||||||
|
monacoRef?: MutableRefObject<typeof monaco>;
|
||||||
|
};
|
||||||
|
|
||||||
|
loader.config({
|
||||||
|
paths: {
|
||||||
|
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Monaco: FC<MonacoProps> = ({
|
||||||
|
id,
|
||||||
|
path = `file:///${id}`,
|
||||||
|
className = id,
|
||||||
|
language = "json",
|
||||||
|
overlay,
|
||||||
|
editorRef,
|
||||||
|
monacoRef,
|
||||||
|
beforeMount,
|
||||||
|
rootProps,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const setTheme = (monaco: Monaco) => {
|
||||||
|
monaco.editor.defineTheme("dark", dark as any);
|
||||||
|
monaco.editor.defineTheme("light", light as any);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
fluid
|
||||||
|
column
|
||||||
|
{...rootProps}
|
||||||
|
css={{
|
||||||
|
position: "relative",
|
||||||
|
height: "100%",
|
||||||
|
...rootProps?.css,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
className={className}
|
||||||
|
language={language}
|
||||||
|
path={path}
|
||||||
|
beforeMount={monaco => {
|
||||||
|
beforeMount?.(monaco);
|
||||||
|
|
||||||
|
setTheme(monaco);
|
||||||
|
}}
|
||||||
|
theme={theme === "dark" ? "dark" : "light"}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{overlay && (
|
||||||
|
<Flex
|
||||||
|
css={{ position: "absolute", bottom: 0, right: 0, width: "100%" }}
|
||||||
|
>
|
||||||
|
{overlay}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Monaco;
|
||||||
@@ -28,6 +28,22 @@ import {
|
|||||||
} from "./Dialog";
|
} from "./Dialog";
|
||||||
import PanelBox from "./PanelBox";
|
import PanelBox from "./PanelBox";
|
||||||
import { templateFileIds } from "../state/constants";
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -91,7 +107,7 @@ const Navigation = () => {
|
|||||||
<Text
|
<Text
|
||||||
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
|
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
|
||||||
>
|
>
|
||||||
{snap.files.length > 0 ? "Gist: " : "Playground"}
|
{snap.files.length > 0 ? "Gist: " : "Builder"}
|
||||||
{snap.files.length > 0 && (
|
{snap.files.length > 0 && (
|
||||||
<Link
|
<Link
|
||||||
href={`https://gist.github.com/${snap.gistOwner || ""}/${
|
href={`https://gist.github.com/${snap.gistOwner || ""}/${
|
||||||
@@ -128,19 +144,20 @@ const Navigation = () => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
css={{
|
css={{
|
||||||
|
display: "flex",
|
||||||
maxWidth: "1080px",
|
maxWidth: "1080px",
|
||||||
width: "80vw",
|
width: "80vw",
|
||||||
height: "80%",
|
maxHeight: "80%",
|
||||||
backgroundColor: "$mauve1 !important",
|
backgroundColor: "$mauve1 !important",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
|
background: "black",
|
||||||
p: 0,
|
p: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
flex: 1,
|
height: "100%",
|
||||||
height: "auto",
|
|
||||||
"@md": {
|
"@md": {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
@@ -151,15 +168,15 @@ const Navigation = () => {
|
|||||||
css={{
|
css={{
|
||||||
borderBottom: "1px solid $colors$mauve5",
|
borderBottom: "1px solid $colors$mauve5",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
minWidth: "240px",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
p: "$7",
|
p: "$7",
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: "$mauve2",
|
||||||
"@md": {
|
"@md": {
|
||||||
width: "30%",
|
width: "30%",
|
||||||
maxWidth: "300px",
|
maxWidth: "300px",
|
||||||
borderBottom: "0px",
|
borderBottom: "0px",
|
||||||
borderRight: "1px solid $colors$mauve6",
|
borderRight: "1px solid $colors$mauve5",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -196,9 +213,9 @@ const Navigation = () => {
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "$3",
|
gap: "$3",
|
||||||
color: "$purple10",
|
color: "$purple11",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
color: "$purple11",
|
color: "$purple12",
|
||||||
},
|
},
|
||||||
"&:focus": {
|
"&:focus": {
|
||||||
outline: 0,
|
outline: 0,
|
||||||
@@ -217,9 +234,9 @@ const Navigation = () => {
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "$3",
|
gap: "$3",
|
||||||
color: "$purple10",
|
color: "$purple11",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
color: "$purple11",
|
color: "$purple12",
|
||||||
},
|
},
|
||||||
"&:focus": {
|
"&:focus": {
|
||||||
outline: 0,
|
outline: 0,
|
||||||
@@ -237,9 +254,9 @@ const Navigation = () => {
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "$3",
|
gap: "$3",
|
||||||
color: "$purple10",
|
color: "$purple11",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
color: "$purple11",
|
color: "$purple12",
|
||||||
},
|
},
|
||||||
"&:focus": {
|
"&:focus": {
|
||||||
outline: 0,
|
outline: 0,
|
||||||
@@ -255,67 +272,42 @@ const Navigation = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div>
|
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr",
|
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",
|
gridTemplateRows: "max-content",
|
||||||
flex: 1,
|
},
|
||||||
p: "$7",
|
"@lg": {
|
||||||
gap: "$3",
|
gridTemplateColumns: "1fr 1fr 1fr",
|
||||||
alignItems: "normal",
|
gridTemplateRows: "max-content",
|
||||||
flexWrap: "wrap",
|
},
|
||||||
backgroundColor: "$mauve1",
|
}}
|
||||||
"@md": {
|
>
|
||||||
gridTemplateColumns: "1fr 1fr 1fr",
|
{Object.values(templateFileIds).map((template) => (
|
||||||
gridTemplateRows: "max-content",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PanelBox
|
<PanelBox
|
||||||
|
key={template.id}
|
||||||
as="a"
|
as="a"
|
||||||
href={`/develop/${templateFileIds.starter}`}
|
href={`/develop/${template.id}`}
|
||||||
>
|
>
|
||||||
<Heading>Starter</Heading>
|
<ImageWrapper>{template.icon()}</ImageWrapper>
|
||||||
<Text>
|
<Heading>{template.name}</Heading>
|
||||||
Just a basic starter with essential imports
|
|
||||||
</Text>
|
<Text>{template.description}</Text>
|
||||||
</PanelBox>
|
</PanelBox>
|
||||||
<PanelBox
|
))}
|
||||||
as="a"
|
</Flex>
|
||||||
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>
|
</Flex>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Box
|
<Box
|
||||||
@@ -348,6 +340,8 @@ const Navigation = () => {
|
|||||||
height: 0,
|
height: 0,
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
},
|
},
|
||||||
|
scrollbarColor: "transparent",
|
||||||
|
scrollbarWidth: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
337
components/RunScript/index.tsx
Normal file
337
components/RunScript/index.tsx
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
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 = (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);
|
||||||
|
|
||||||
|
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 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 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(() => {
|
||||||
|
try {
|
||||||
|
let data: any = {};
|
||||||
|
Object.keys(fields).forEach(key => {
|
||||||
|
data[key] = fields[key].value;
|
||||||
|
});
|
||||||
|
const template = 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" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}, [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}
|
||||||
|
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;
|
||||||
@@ -91,7 +91,7 @@ const Select = forwardRef<any, Props>((props, ref) => {
|
|||||||
...provided,
|
...provided,
|
||||||
color: colors.searchText,
|
color: colors.searchText,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
state.isSelected || state.isFocused
|
state.isFocused
|
||||||
? colors.activeLight
|
? colors.activeLight
|
||||||
: colors.dropDownBg,
|
: colors.dropDownBg,
|
||||||
":hover": {
|
":hover": {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { Plus, Trash, X } from "phosphor-react";
|
import { Plus, Trash, X } from "phosphor-react";
|
||||||
import Button from "./Button";
|
import { Button, Box, Text } from ".";
|
||||||
import Box from "./Box";
|
|
||||||
import { Stack, Flex, Select } from ".";
|
import { Stack, Flex, Select } from ".";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -19,209 +18,364 @@ import {
|
|||||||
useForm,
|
useForm,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
|
||||||
import { TTS, tts } from "../utils/hookOnCalculator";
|
|
||||||
import { deployHook } from "../state/actions";
|
import { deployHook } from "../state/actions";
|
||||||
import type { IAccount } from "../state";
|
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import state from "../state";
|
import state, { IFile, SelectOption } from "../state";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { sha256 } from "../state/actions/deployHook";
|
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";
|
||||||
|
|
||||||
const transactionOptions = Object.keys(tts).map((key) => ({
|
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||||
label: key,
|
({ accountAddress }) => {
|
||||||
value: key as keyof TTS,
|
const snap = useSnapshot(state);
|
||||||
}));
|
|
||||||
|
|
||||||
export type SetHookData = {
|
const [estimateLoading, setEstimateLoading] = useState(false);
|
||||||
Invoke: {
|
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
|
||||||
value: keyof TTS;
|
|
||||||
label: string;
|
const compiledFiles = snap.files.filter(file => file.compiledContent);
|
||||||
}[];
|
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
|
||||||
HookNamespace: string;
|
|
||||||
HookParameters: {
|
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
|
||||||
HookParameter: {
|
label: acc.name,
|
||||||
HookParameterName: string;
|
value: acc.address,
|
||||||
HookParameterValue: string;
|
}));
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}[];
|
|
||||||
// HookGrants: {
|
|
||||||
// HookGrant: {
|
|
||||||
// Authorize: string;
|
|
||||||
// HookHash: string;
|
|
||||||
// };
|
|
||||||
// }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
|
const onSubmit: SubmitHandler<SetHookData> = async data => {
|
||||||
const snap = useSnapshot(state);
|
const currAccount = state.accounts.find(
|
||||||
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
|
acc => acc.address === account?.address
|
||||||
const {
|
);
|
||||||
register,
|
if (!account) return;
|
||||||
handleSubmit,
|
if (currAccount) currAccount.isLoading = true;
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<SetHookData>({
|
|
||||||
defaultValues: {
|
|
||||||
HookNamespace: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
|
||||||
control,
|
|
||||||
name: "HookParameters", // unique name for your Field Array
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update value if activeWat changes
|
data.HookParameters.forEach(param => {
|
||||||
useEffect(() => {
|
delete param.$metaData;
|
||||||
setValue(
|
return param;
|
||||||
"HookNamespace",
|
});
|
||||||
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
|
|
||||||
);
|
|
||||||
}, [snap.activeWat, snap.files, 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",
|
|
||||||
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
|
|
||||||
);
|
|
||||||
const calculateHashedValue = useCallback(async () => {
|
|
||||||
const hashedVal = await sha256(namespace);
|
|
||||||
setHashedNamespace(hashedVal.toUpperCase());
|
|
||||||
}, [namespace]);
|
|
||||||
useEffect(() => {
|
|
||||||
calculateHashedValue();
|
|
||||||
}, [namespace, calculateHashedValue]);
|
|
||||||
|
|
||||||
if (!account) {
|
const res = await deployHook(account, data);
|
||||||
return null;
|
if (currAccount) currAccount.isLoading = false;
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<SetHookData> = async (data) => {
|
if (res && res.engine_result === "tesSUCCESS") {
|
||||||
const currAccount = state.accounts.find(
|
toast.success("Transaction succeeded!");
|
||||||
(acc) => acc.address === account.address
|
return setIsSetHookDialogOpen(false);
|
||||||
);
|
}
|
||||||
if (currAccount) currAccount.isLoading = true;
|
toast.error(`Transaction failed! (${res?.engine_result_message})`);
|
||||||
const res = await deployHook(account, data);
|
};
|
||||||
if (currAccount) currAccount.isLoading = false;
|
|
||||||
|
|
||||||
if (res && res.engine_result === "tesSUCCESS") {
|
const onOpenChange = useCallback((open: boolean) => {
|
||||||
toast.success("Transaction succeeded!");
|
setIsSetHookDialogOpen(open);
|
||||||
return setIsSetHookDialogOpen(false);
|
|
||||||
}
|
|
||||||
toast.error(`Transaction failed! (${res?.engine_result_message})`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
if (open) calculateFee();
|
||||||
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
|
}, [calculateFee]);
|
||||||
<DialogTrigger asChild>
|
return (
|
||||||
<Button
|
<Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
|
||||||
ghost
|
<DialogTrigger asChild>
|
||||||
size="xs"
|
<Button
|
||||||
uppercase
|
ghost
|
||||||
variant={"secondary"}
|
size="xs"
|
||||||
disabled={
|
uppercase
|
||||||
account.isLoading ||
|
variant={"secondary"}
|
||||||
!snap.files.filter((file) => file.compiledWatContent).length
|
disabled={
|
||||||
}
|
!account || account.isLoading || !activeFile || tooLargeFile()
|
||||||
>
|
}
|
||||||
Set Hook
|
>
|
||||||
</Button>
|
Set Hook
|
||||||
</DialogTrigger>
|
</Button>
|
||||||
<DialogContent>
|
</DialogTrigger>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<DialogContent>
|
||||||
<DialogTitle>Deploy configuration</DialogTitle>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<DialogDescription as="div">
|
<DialogTitle>Deploy configuration</DialogTitle>
|
||||||
<Stack css={{ width: "100%", flex: 1 }}>
|
<DialogDescription as="div">
|
||||||
<Box css={{ width: "100%" }}>
|
<Stack css={{ width: "100%", flex: 1 }}>
|
||||||
<Label>Invoke on transactions</Label>
|
<Box css={{ width: "100%" }}>
|
||||||
<Controller
|
<Label>Account</Label>
|
||||||
name="Invoke"
|
<Select
|
||||||
control={control}
|
instanceId="deploy-account"
|
||||||
defaultValue={transactionOptions.filter(
|
placeholder="Select account"
|
||||||
(to) => to.label === "ttPAYMENT"
|
options={accountOptions}
|
||||||
)}
|
value={selectedAccount}
|
||||||
render={({ field }) => (
|
onChange={(acc: any) => setSelectedAccount(acc)}
|
||||||
<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"}
|
|
||||||
defaultValue={
|
|
||||||
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{errors.HookNamespace?.type === "required" && (
|
|
||||||
<Box css={{ display: "inline", color: "$red11" }}>
|
|
||||||
Namespace is required
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Box css={{ mt: "$3" }}>
|
|
||||||
<Label>Hook Namespace (sha256)</Label>
|
|
||||||
<Input readOnly value={hashedNamespace} />
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
<Box css={{ width: "100%" }}>
|
||||||
<Box css={{ width: "100%" }}>
|
<Label>Invoke on transactions</Label>
|
||||||
<Label style={{ marginBottom: "10px", display: "block" }}>
|
<Controller
|
||||||
Hook parameters
|
name="Invoke"
|
||||||
</Label>
|
control={control}
|
||||||
<Stack>
|
render={({ field }) => (
|
||||||
{fields.map((field, index) => (
|
<Select
|
||||||
<Stack key={field.id}>
|
{...field}
|
||||||
<Input
|
closeMenuOnSelect={false}
|
||||||
// important to include key with field's id
|
isMulti
|
||||||
placeholder="Parameter name"
|
menuPosition="fixed"
|
||||||
{...register(
|
options={transactionOptions}
|
||||||
`HookParameters.${index}.HookParameter.HookParameterName`
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
)}
|
||||||
placeholder="Value (hex-quoted)"
|
/>
|
||||||
{...register(
|
</Box>
|
||||||
`HookParameters.${index}.HookParameter.HookParameterValue`
|
<Box css={{ width: "100%" }}>
|
||||||
)}
|
<Label>Hook Namespace Seed</Label>
|
||||||
/>
|
<Input
|
||||||
<Button onClick={() => remove(index)} variant="destroy">
|
{...register("HookNamespace", { required: true })}
|
||||||
<Trash weight="regular" size="16px" />
|
autoComplete={"off"}
|
||||||
</Button>
|
/>
|
||||||
</Stack>
|
{errors.HookNamespace?.type === "required" && (
|
||||||
))}
|
<Box css={{ display: "inline", color: "$red11" }}>
|
||||||
<Button
|
Namespace is required
|
||||||
outline
|
</Box>
|
||||||
fullWidth
|
)}
|
||||||
type="button"
|
<Box css={{ mt: "$3" }}>
|
||||||
onClick={() =>
|
<Label>Hook Namespace (sha256)</Label>
|
||||||
append({
|
<Input readOnly value={hashedNamespace} />
|
||||||
HookParameter: {
|
</Box>
|
||||||
HookParameterName: "",
|
</Box>
|
||||||
HookParameterValue: "",
|
|
||||||
|
<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,
|
||||||
<Plus size="16px" />
|
},
|
||||||
Add Hook Parameter
|
}}
|
||||||
</Button>
|
/>
|
||||||
</Stack>
|
<Button
|
||||||
</Box>
|
size="xs"
|
||||||
{/* <Box css={{ width: "100%" }}>
|
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%" }}>
|
||||||
<label style={{ marginBottom: "10px", display: "block" }}>
|
<label style={{ marginBottom: "10px", display: "block" }}>
|
||||||
Hook Grants
|
Hook Grants
|
||||||
</label>
|
</label>
|
||||||
@@ -269,38 +423,41 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box> */}
|
</Box> */}
|
||||||
</Stack>
|
</Stack>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
marginTop: 25,
|
marginTop: 25,
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
gap: "$3",
|
gap: "$3",
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button outline>Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
{/* <DialogClose asChild> */}
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
type="submit"
|
|
||||||
isLoading={account.isLoading}
|
|
||||||
>
|
>
|
||||||
Set Hook
|
<DialogClose asChild>
|
||||||
</Button>
|
<Button outline>Cancel</Button>
|
||||||
{/* </DialogClose> */}
|
</DialogClose>
|
||||||
</Flex>
|
{/* <DialogClose asChild> */}
|
||||||
<DialogClose asChild>
|
<Button
|
||||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
variant="primary"
|
||||||
<X size="20px" />
|
type="submit"
|
||||||
</Box>
|
isLoading={account?.isLoading}
|
||||||
</DialogClose>
|
>
|
||||||
</form>
|
Set Hook
|
||||||
</DialogContent>
|
</Button>
|
||||||
</Dialog>
|
{/* </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;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { ReactNode, ReactElement } from "react";
|
import type { ReactNode, ReactElement } from "react";
|
||||||
import { Box, Button, Flex, Input, Label, Stack, Text } from ".";
|
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from ".";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
} from "./Dialog";
|
} from "./Dialog";
|
||||||
import { Plus, X } from "phosphor-react";
|
import { Plus, X } from "phosphor-react";
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from "../stitches.config";
|
||||||
|
import { capitalize } from "../utils/helpers";
|
||||||
|
import ContextMenu, { ContentMenuOption } from "./ContextMenu";
|
||||||
|
|
||||||
const ErrorText = styled(Text, {
|
const ErrorText = styled(Text, {
|
||||||
color: "$error",
|
color: "$error",
|
||||||
@@ -24,21 +26,30 @@ const ErrorText = styled(Text, {
|
|||||||
display: "block",
|
display: "block",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type Nullable<T> = T | null | undefined | false;
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
header?: string;
|
header: string;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO customise messages shown
|
// TODO customize messages shown
|
||||||
interface Props {
|
interface Props {
|
||||||
|
label?: string;
|
||||||
activeIndex?: number;
|
activeIndex?: number;
|
||||||
activeHeader?: string;
|
activeHeader?: string;
|
||||||
headless?: boolean;
|
headless?: boolean;
|
||||||
children: ReactElement<TabProps>[];
|
children: ReactElement<TabProps>[];
|
||||||
keepAllAlive?: boolean;
|
keepAllAlive?: boolean;
|
||||||
defaultExtension?: string;
|
defaultExtension?: string;
|
||||||
forceDefaultExtension?: boolean;
|
extensionRequired?: boolean;
|
||||||
|
allowedExtensions?: string[];
|
||||||
|
headerExtraValidation?: {
|
||||||
|
regex: string | RegExp;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
onCreateNewTab?: (name: string) => any;
|
onCreateNewTab?: (name: string) => any;
|
||||||
|
onRenameTab?: (index: number, nwName: string, oldName?: string) => any;
|
||||||
onCloseTab?: (index: number, header?: string) => any;
|
onCloseTab?: (index: number, header?: string) => any;
|
||||||
onChangeActive?: (index: number, header?: string) => any;
|
onChangeActive?: (index: number, header?: string) => any;
|
||||||
}
|
}
|
||||||
@@ -46,6 +57,7 @@ interface Props {
|
|||||||
export const Tab = (props: TabProps) => null;
|
export const Tab = (props: TabProps) => null;
|
||||||
|
|
||||||
export const Tabs = ({
|
export const Tabs = ({
|
||||||
|
label = "Tab",
|
||||||
children,
|
children,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
activeHeader,
|
activeHeader,
|
||||||
@@ -54,15 +66,19 @@ export const Tabs = ({
|
|||||||
onCreateNewTab,
|
onCreateNewTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onChangeActive,
|
onChangeActive,
|
||||||
|
onRenameTab,
|
||||||
|
headerExtraValidation,
|
||||||
|
extensionRequired,
|
||||||
defaultExtension = "",
|
defaultExtension = "",
|
||||||
forceDefaultExtension,
|
allowedExtensions,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [active, setActive] = useState(activeIndex || 0);
|
const [active, setActive] = useState(activeIndex || 0);
|
||||||
const tabs: TabProps[] = children.map(elem => elem.props);
|
const tabs: TabProps[] = children.map(elem => elem.props);
|
||||||
|
|
||||||
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
|
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
|
||||||
|
const [renamingTab, setRenamingTab] = useState<number | null>(null);
|
||||||
const [tabname, setTabname] = useState("");
|
const [tabname, setTabname] = useState("");
|
||||||
const [newtabError, setNewtabError] = useState<string | null>(null);
|
const [tabnameError, setTabnameError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeIndex) setActive(activeIndex);
|
if (activeIndex) setActive(activeIndex);
|
||||||
@@ -78,17 +94,46 @@ export const Tabs = ({
|
|||||||
|
|
||||||
// when filename changes, reset error
|
// when filename changes, reset error
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNewtabError(null);
|
setTabnameError(null);
|
||||||
}, [tabname, setNewtabError]);
|
}, [tabname, setTabnameError]);
|
||||||
|
|
||||||
const validateTabname = useCallback(
|
const validateTabname = useCallback(
|
||||||
(tabname: string): { error: string | null } => {
|
(tabname: string): { error?: string, result?: string } => {
|
||||||
if (tabs.find(tab => tab.header === tabname)) {
|
if (!tabname) {
|
||||||
return { error: "Name already exists." };
|
return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
|
||||||
}
|
}
|
||||||
return { error: null };
|
let ext =
|
||||||
|
(tabname.includes(".") && tabname.split(".").pop()) || "";
|
||||||
|
|
||||||
|
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 && !allowedExtensions.includes(ext)) {
|
||||||
|
return { error: "This file extension is not allowed!" };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
headerExtraValidation &&
|
||||||
|
!tabname.match(headerExtraValidation.regex)
|
||||||
|
) {
|
||||||
|
return { error: headerExtraValidation.error };
|
||||||
|
}
|
||||||
|
return { result: tabname };
|
||||||
},
|
},
|
||||||
[tabs]
|
[
|
||||||
|
allowedExtensions,
|
||||||
|
defaultExtension,
|
||||||
|
extensionRequired,
|
||||||
|
headerExtraValidation,
|
||||||
|
label,
|
||||||
|
tabs,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleActiveChange = useCallback(
|
const handleActiveChange = useCallback(
|
||||||
@@ -99,35 +144,41 @@ export const Tabs = ({
|
|||||||
[onChangeActive]
|
[onChangeActive]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateTab = useCallback(() => {
|
const handleRenameTab = useCallback(() => {
|
||||||
// add default extension in case omitted
|
if (renamingTab === null) return;
|
||||||
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
|
|
||||||
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
|
|
||||||
_tabname = _tabname + defaultExtension;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chk = validateTabname(_tabname);
|
const res = validateTabname(tabname);
|
||||||
if (chk.error) {
|
if (res.error) {
|
||||||
setNewtabError(`Error: ${chk.error}`);
|
setTabnameError(`Error: ${res.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { result: _tabname = tabname } = res
|
||||||
|
|
||||||
|
setRenamingTab(null);
|
||||||
|
setTabname("");
|
||||||
|
|
||||||
|
const oldName = tabs[renamingTab]?.header;
|
||||||
|
onRenameTab?.(renamingTab, _tabname, oldName);
|
||||||
|
|
||||||
|
handleActiveChange(renamingTab);
|
||||||
|
}, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname]);
|
||||||
|
|
||||||
|
const handleCreateTab = useCallback(() => {
|
||||||
|
const res = validateTabname(tabname);
|
||||||
|
if (res.error) {
|
||||||
|
setTabnameError(`Error: ${res.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { result: _tabname = tabname } = res
|
||||||
|
|
||||||
setIsNewtabDialogOpen(false);
|
setIsNewtabDialogOpen(false);
|
||||||
setTabname("");
|
setTabname("");
|
||||||
|
|
||||||
onCreateNewTab?.(_tabname);
|
onCreateNewTab?.(_tabname);
|
||||||
|
|
||||||
// switch to new tab?
|
|
||||||
handleActiveChange(tabs.length, _tabname);
|
handleActiveChange(tabs.length, _tabname);
|
||||||
}, [
|
}, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length]);
|
||||||
tabname,
|
|
||||||
defaultExtension,
|
|
||||||
forceDefaultExtension,
|
|
||||||
validateTabname,
|
|
||||||
onCreateNewTab,
|
|
||||||
handleActiveChange,
|
|
||||||
tabs.length,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleCloseTab = useCallback(
|
const handleCloseTab = useCallback(
|
||||||
(idx: number) => {
|
(idx: number) => {
|
||||||
@@ -136,10 +187,27 @@ export const Tabs = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onCloseTab?.(idx, tabs[idx].header);
|
onCloseTab?.(idx, tabs[idx].header);
|
||||||
|
|
||||||
|
handleActiveChange(idx, tabs[idx].header);
|
||||||
},
|
},
|
||||||
[active, onCloseTab, tabs]
|
[active, handleActiveChange, onCloseTab, tabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const closeOption = (idx: number): Nullable<ContentMenuOption> =>
|
||||||
|
onCloseTab && {
|
||||||
|
type: "text",
|
||||||
|
label: "Close",
|
||||||
|
key: "close",
|
||||||
|
onSelect: () => handleCloseTab(idx),
|
||||||
|
};
|
||||||
|
const renameOption = (idx: number): Nullable<ContentMenuOption> =>
|
||||||
|
onRenameTab && {
|
||||||
|
type: "text",
|
||||||
|
label: "Rename",
|
||||||
|
key: "rename",
|
||||||
|
onSelect: () => setRenamingTab(idx),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!headless && (
|
{!headless && (
|
||||||
@@ -154,46 +222,54 @@ export const Tabs = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, idx) => (
|
{tabs.map((tab, idx) => (
|
||||||
<Button
|
<ContextMenu
|
||||||
key={tab.header}
|
key={tab.header}
|
||||||
role="tab"
|
options={
|
||||||
tabIndex={idx}
|
[closeOption(idx), renameOption(idx)].filter(
|
||||||
onClick={() => handleActiveChange(idx, tab.header)}
|
Boolean
|
||||||
onKeyPress={() => handleActiveChange(idx, tab.header)}
|
) as ContentMenuOption[]
|
||||||
outline={active !== idx}
|
}
|
||||||
size="sm"
|
|
||||||
css={{
|
|
||||||
"&:hover": {
|
|
||||||
span: {
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{tab.header || idx}
|
<Button
|
||||||
{onCloseTab && (
|
role="tab"
|
||||||
<Box
|
tabIndex={idx}
|
||||||
as="span"
|
onClick={() => handleActiveChange(idx, tab.header)}
|
||||||
css={{
|
onKeyPress={() => handleActiveChange(idx, tab.header)}
|
||||||
display: "flex",
|
outline={active !== idx}
|
||||||
p: "2px",
|
size="sm"
|
||||||
borderRadius: "$full",
|
css={{
|
||||||
mr: "-4px",
|
"&:hover": {
|
||||||
"&:hover": {
|
span: {
|
||||||
// boxSizing: "0px 0px 1px",
|
visibility: "visible",
|
||||||
backgroundColor: "$mauve2",
|
|
||||||
color: "$mauve12",
|
|
||||||
},
|
},
|
||||||
}}
|
},
|
||||||
onClick={(ev: React.MouseEvent<HTMLElement>) => {
|
}}
|
||||||
ev.stopPropagation();
|
>
|
||||||
handleCloseTab(idx);
|
{tab.header || idx}
|
||||||
}}
|
{onCloseTab && (
|
||||||
>
|
<Box
|
||||||
<X size="9px" weight="bold" />
|
as="span"
|
||||||
</Box>
|
css={{
|
||||||
)}
|
display: "flex",
|
||||||
</Button>
|
p: "2px",
|
||||||
|
borderRadius: "$full",
|
||||||
|
mr: "-4px",
|
||||||
|
"&:hover": {
|
||||||
|
// boxSizing: "0px 0px 1px",
|
||||||
|
backgroundColor: "$mauve2",
|
||||||
|
color: "$mauve12",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={(ev: React.MouseEvent<HTMLElement>) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
handleCloseTab(idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size="9px" weight="bold" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</ContextMenu>
|
||||||
))}
|
))}
|
||||||
{onCreateNewTab && (
|
{onCreateNewTab && (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -206,13 +282,16 @@ export const Tabs = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
css={{ alignItems: "center", px: "$2", mr: "$3" }}
|
css={{ alignItems: "center", px: "$2", mr: "$3" }}
|
||||||
>
|
>
|
||||||
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
|
<Plus size="16px" />{" "}
|
||||||
|
{tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Create new tab</DialogTitle>
|
<DialogTitle>
|
||||||
|
Create new {label.toLocaleLowerCase()}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Label>Tabname</Label>
|
<Label>{label} name</Label>
|
||||||
<Input
|
<Input
|
||||||
value={tabname}
|
value={tabname}
|
||||||
onChange={e => setTabname(e.target.value)}
|
onChange={e => setTabname(e.target.value)}
|
||||||
@@ -222,7 +301,7 @@ export const Tabs = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ErrorText>{newtabError}</ErrorText>
|
<ErrorText>{tabnameError}</ErrorText>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
@@ -247,31 +326,79 @@ export const Tabs = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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" }}>
|
||||||
|
<X size="20px" />
|
||||||
|
</Box>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{keepAllAlive ? (
|
{keepAllAlive
|
||||||
tabs.map((tab, idx) => {
|
? tabs.map((tab, idx) => {
|
||||||
// TODO Maybe rule out fragments as children
|
// TODO Maybe rule out fragments as children
|
||||||
if (!isValidElement(tab.children)) {
|
if (!isValidElement(tab.children)) {
|
||||||
if (active !== idx) return null;
|
if (active !== idx) return null;
|
||||||
return tab.children;
|
return tab.children;
|
||||||
}
|
}
|
||||||
let key = tab.children.key || tab.header || idx;
|
let key = tab.children.key || tab.header || idx;
|
||||||
let { children } = tab;
|
let { children } = tab;
|
||||||
let { style, ...props } = children.props;
|
let { style, ...props } = children.props;
|
||||||
return (
|
return (
|
||||||
<children.type
|
<children.type
|
||||||
key={key}
|
key={key}
|
||||||
{...props}
|
{...props}
|
||||||
style={{ ...style, display: active !== idx ? "none" : undefined }}
|
style={{
|
||||||
/>
|
...style,
|
||||||
);
|
display: active !== idx ? "none" : undefined,
|
||||||
})
|
}}
|
||||||
) : (
|
/>
|
||||||
<Fragment key={tabs[active].header || active}>
|
);
|
||||||
{tabs[active].children}
|
})
|
||||||
</Fragment>
|
: tabs[active] && (
|
||||||
)}
|
<Fragment key={tabs[active].header || active}>
|
||||||
|
{tabs[active].children}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,20 +7,35 @@ const Text = styled("span", {
|
|||||||
variants: {
|
variants: {
|
||||||
small: {
|
small: {
|
||||||
true: {
|
true: {
|
||||||
fontSize: '$xs'
|
fontSize: "$xs",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
true: {
|
true: {
|
||||||
color: '$mauve9'
|
color: "$mauve9",
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
true: {
|
||||||
|
color: "$error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
true: {
|
||||||
|
color: "$warning",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
monospace: {
|
monospace: {
|
||||||
true: {
|
true: {
|
||||||
fontFamily: '$monospace'
|
fontFamily: "$monospace",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
block: {
|
||||||
|
true: {
|
||||||
|
display: "block",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Text;
|
export default Text;
|
||||||
|
|||||||
115
components/Textarea.tsx
Normal file
115
components/Textarea.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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;
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Play } from "phosphor-react";
|
import { Play } from "phosphor-react";
|
||||||
import { FC, useCallback, useEffect, useMemo } from "react";
|
import { FC, useCallback, useEffect } from "react";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import state from "../../state";
|
import state from "../../state";
|
||||||
import {
|
import {
|
||||||
modifyTransaction,
|
defaultTransactionType,
|
||||||
|
getTxFields,
|
||||||
|
modifyTxState,
|
||||||
prepareState,
|
prepareState,
|
||||||
prepareTransaction,
|
prepareTransaction,
|
||||||
|
SelectOption,
|
||||||
TransactionState,
|
TransactionState,
|
||||||
} from "../../state/transactions";
|
} from "../../state/transactions";
|
||||||
import { sendTransaction } from "../../state/actions";
|
import { sendTransaction } from "../../state/actions";
|
||||||
@@ -14,6 +17,8 @@ import Button from "../Button";
|
|||||||
import Flex from "../Flex";
|
import Flex from "../Flex";
|
||||||
import { TxJson } from "./json";
|
import { TxJson } from "./json";
|
||||||
import { TxUI } from "./ui";
|
import { TxUI } from "./ui";
|
||||||
|
import { default as _estimateFee } from "../../utils/estimateFee";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export interface TransactionProps {
|
export interface TransactionProps {
|
||||||
header: string;
|
header: string;
|
||||||
@@ -32,19 +37,18 @@ const Transaction: FC<TransactionProps> = ({
|
|||||||
txIsDisabled,
|
txIsDisabled,
|
||||||
txIsLoading,
|
txIsLoading,
|
||||||
viewType,
|
viewType,
|
||||||
editorSavedValue,
|
|
||||||
editorValue,
|
editorValue,
|
||||||
} = txState;
|
} = txState;
|
||||||
|
|
||||||
const setState = useCallback(
|
const setState = useCallback(
|
||||||
(pTx?: Partial<TransactionState>) => {
|
(pTx?: Partial<TransactionState>) => {
|
||||||
return modifyTransaction(header, pTx);
|
return modifyTxState(header, pTx);
|
||||||
},
|
},
|
||||||
[header]
|
[header]
|
||||||
);
|
);
|
||||||
|
|
||||||
const prepareOptions = useCallback(
|
const prepareOptions = useCallback(
|
||||||
(state: TransactionState = txState) => {
|
(state: Partial<TransactionState> = txState) => {
|
||||||
const {
|
const {
|
||||||
selectedTransaction,
|
selectedTransaction,
|
||||||
selectedDestAccount,
|
selectedDestAccount,
|
||||||
@@ -53,9 +57,7 @@ const Transaction: FC<TransactionProps> = ({
|
|||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
const TransactionType = selectedTransaction?.value || null;
|
const TransactionType = selectedTransaction?.value || null;
|
||||||
const Destination =
|
const Destination = selectedDestAccount?.value || txFields?.Destination;
|
||||||
selectedDestAccount?.value ||
|
|
||||||
("Destination" in txFields ? null : undefined);
|
|
||||||
const Account = selectedAccount?.value || null;
|
const Account = selectedAccount?.value || null;
|
||||||
|
|
||||||
return prepareTransaction({
|
return prepareTransaction({
|
||||||
@@ -76,13 +78,19 @@ const Transaction: FC<TransactionProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
setState({ txIsDisabled: false });
|
setState({ txIsDisabled: false });
|
||||||
}
|
}
|
||||||
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading]);
|
}, [
|
||||||
|
selectedAccount?.value,
|
||||||
|
selectedTransaction?.value,
|
||||||
|
setState,
|
||||||
|
txIsLoading,
|
||||||
|
]);
|
||||||
|
|
||||||
const submitTest = useCallback(async () => {
|
const submitTest = useCallback(async () => {
|
||||||
let st: TransactionState | undefined;
|
let st: TransactionState | undefined;
|
||||||
|
const tt = txState.selectedTransaction?.value;
|
||||||
if (viewType === "json") {
|
if (viewType === "json") {
|
||||||
// save the editor state first
|
// save the editor state first
|
||||||
const pst = prepareState(editorValue || '', txState);
|
const pst = prepareState(editorValue || "", tt);
|
||||||
if (!pst) return;
|
if (!pst) return;
|
||||||
|
|
||||||
st = setState(pst);
|
st = setState(pst);
|
||||||
@@ -101,8 +109,9 @@ const Transaction: FC<TransactionProps> = ({
|
|||||||
}
|
}
|
||||||
const options = prepareOptions(st);
|
const options = prepareOptions(st);
|
||||||
|
|
||||||
if (options.Destination === null) {
|
const fields = getTxFields(options.TransactionType);
|
||||||
throw Error("Destination account cannot be null")
|
if (fields.Destination && !options.Destination) {
|
||||||
|
throw Error("Destination account is required!");
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendTransaction(account, options, { logPrefix });
|
await sendTransaction(account, options, { logPrefix });
|
||||||
@@ -116,30 +125,91 @@ const Transaction: FC<TransactionProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState({ txIsLoading: false });
|
setState({ txIsLoading: false });
|
||||||
}, [viewType, accounts, txIsDisabled, setState, header, editorValue, txState, selectedAccount?.value, prepareOptions]);
|
}, [
|
||||||
|
viewType,
|
||||||
|
accounts,
|
||||||
|
txIsDisabled,
|
||||||
|
setState,
|
||||||
|
header,
|
||||||
|
editorValue,
|
||||||
|
txState,
|
||||||
|
selectedAccount?.value,
|
||||||
|
prepareOptions,
|
||||||
|
]);
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
const getJsonString = useCallback(
|
||||||
modifyTransaction(header, { viewType }, { replaceState: true });
|
(state?: Partial<TransactionState>) =>
|
||||||
}, [header, viewType]);
|
JSON.stringify(
|
||||||
|
prepareOptions?.(state) || {},
|
||||||
|
null,
|
||||||
|
editorSettings.tabSize
|
||||||
|
),
|
||||||
|
[editorSettings.tabSize, prepareOptions]
|
||||||
|
);
|
||||||
|
|
||||||
const jsonValue = useMemo(
|
const resetState = useCallback(
|
||||||
() =>
|
(transactionType: SelectOption | undefined = defaultTransactionType) => {
|
||||||
editorSavedValue ||
|
const fields = getTxFields(transactionType?.value);
|
||||||
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize),
|
|
||||||
[editorSavedValue, editorSettings.tabSize, prepareOptions]
|
const nwState: Partial<TransactionState> = {
|
||||||
|
viewType,
|
||||||
|
selectedTransaction: transactionType,
|
||||||
|
selectedDestAccount: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Currently in schema "Destination": "SomeVal" means 'Destination is required' while empty string indicates it is optional
|
||||||
|
// TODO Update schema with clear required tag
|
||||||
|
if (fields.Destination !== undefined) {
|
||||||
|
fields.Destination = "";
|
||||||
|
} else {
|
||||||
|
fields.Destination = 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 (
|
return (
|
||||||
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
|
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
|
||||||
{viewType === "json" ? (
|
{viewType === "json" ? (
|
||||||
<TxJson
|
<TxJson
|
||||||
value={jsonValue}
|
getJsonString={getJsonString}
|
||||||
header={header}
|
header={header}
|
||||||
state={txState}
|
state={txState}
|
||||||
setState={setState}
|
setState={setState}
|
||||||
|
estimateFee={estimateFee}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TxUI state={txState} setState={setState} />
|
<TxUI state={txState} setState={setState} estimateFee={estimateFee} />
|
||||||
)}
|
)}
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
@@ -155,7 +225,7 @@ const Transaction: FC<TransactionProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (viewType === "ui") {
|
if (viewType === "ui") {
|
||||||
setState({ editorSavedValue: null, viewType: "json" });
|
setState({ viewType: "json" });
|
||||||
} else setState({ viewType: "ui" });
|
} else setState({ viewType: "ui" });
|
||||||
}}
|
}}
|
||||||
outline
|
outline
|
||||||
@@ -163,7 +233,7 @@ const Transaction: FC<TransactionProps> = ({
|
|||||||
{viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"}
|
{viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"}
|
||||||
</Button>
|
</Button>
|
||||||
<Flex row>
|
<Flex row>
|
||||||
<Button onClick={resetState} outline css={{ mr: "$3" }}>
|
<Button onClick={() => resetState()} outline css={{ mr: "$3" }}>
|
||||||
RESET
|
RESET
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import Editor, { loader, useMonaco } from "@monaco-editor/react";
|
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { FC, useCallback, useEffect, useState } from "react";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
import dark from "../../theme/editor/amy.json";
|
|
||||||
import light from "../../theme/editor/xcode_default.json";
|
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import state, {
|
import state, {
|
||||||
prepareState,
|
prepareState,
|
||||||
@@ -11,80 +6,90 @@ import state, {
|
|||||||
TransactionState,
|
TransactionState,
|
||||||
} from "../../state";
|
} from "../../state";
|
||||||
import Text from "../Text";
|
import Text from "../Text";
|
||||||
import Flex from "../Flex";
|
import { Flex, Link } from "..";
|
||||||
import { Link } from "..";
|
|
||||||
import { showAlert } from "../../state/actions/showAlert";
|
import { showAlert } from "../../state/actions/showAlert";
|
||||||
import { parseJSON } from "../../utils/json";
|
import { parseJSON } from "../../utils/json";
|
||||||
import { extractSchemaProps } from "../../utils/schema";
|
import { extractSchemaProps } from "../../utils/schema";
|
||||||
import amountSchema from "../../content/amount-schema.json";
|
import amountSchema from "../../content/amount-schema.json";
|
||||||
|
import Monaco from "../Monaco";
|
||||||
loader.config({
|
import type monaco from "monaco-editor";
|
||||||
paths: {
|
|
||||||
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface JsonProps {
|
interface JsonProps {
|
||||||
value?: string;
|
getJsonString?: (state?: Partial<TransactionState>) => string;
|
||||||
header?: string;
|
header?: string;
|
||||||
setState: (pTx?: Partial<TransactionState> | undefined) => void;
|
setState: (pTx?: Partial<TransactionState> | undefined) => void;
|
||||||
state: TransactionState;
|
state: TransactionState;
|
||||||
|
estimateFee?: () => Promise<string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TxJson: FC<JsonProps> = ({
|
export const TxJson: FC<JsonProps> = ({
|
||||||
value = "",
|
getJsonString,
|
||||||
state: txState,
|
state: txState,
|
||||||
header,
|
header,
|
||||||
setState,
|
setState,
|
||||||
}) => {
|
}) => {
|
||||||
const { editorSettings, accounts } = useSnapshot(state);
|
const { editorSettings, accounts } = useSnapshot(state);
|
||||||
const { editorValue = value, selectedTransaction } = txState;
|
const { editorValue, estimatedFee } = txState;
|
||||||
const { theme } = useTheme();
|
const [currTxType, setCurrTxType] = useState<string | undefined>(
|
||||||
const [hasUnsaved, setHasUnsaved] = useState(false);
|
txState.selectedTransaction?.value
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState({ editorValue: value });
|
setState({
|
||||||
|
editorValue: getJsonString?.(),
|
||||||
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [value]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorValue === value) setHasUnsaved(false);
|
const parsed = parseJSON(editorValue);
|
||||||
else setHasUnsaved(true);
|
if (!parsed) return;
|
||||||
}, [editorValue, value]);
|
|
||||||
|
|
||||||
const saveState = (value: string, txState: TransactionState) => {
|
const tt = parsed.TransactionType;
|
||||||
const tx = prepareState(value, txState);
|
const tx = transactionsData.find(t => t.TransactionType === tt);
|
||||||
if (tx) setState(tx);
|
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 = () => {
|
const discardChanges = () => {
|
||||||
showAlert("Confirm", {
|
showAlert("Confirm", {
|
||||||
body: "Are you sure to discard these changes?",
|
body: "Are you sure to discard these changes?",
|
||||||
confirmText: "Yes",
|
confirmText: "Yes",
|
||||||
onConfirm: () => setState({ editorValue: value }),
|
onCancel: () => {},
|
||||||
|
onConfirm: () => setState({ editorValue: getJsonString?.() }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onExit = (value: string) => {
|
const onExit = (value: string) => {
|
||||||
const options = parseJSON(value);
|
const options = parseJSON(value);
|
||||||
if (options) {
|
if (options) {
|
||||||
saveState(value, txState);
|
saveState(value, currTxType);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showAlert("Error!", {
|
showAlert("Error!", {
|
||||||
body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
|
body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
|
||||||
confirmText: "Discard",
|
confirmText: "Discard",
|
||||||
onConfirm: () => setState({ editorValue: value }),
|
onConfirm: () => setState({ editorValue: getJsonString?.() }),
|
||||||
onCancel: () => setState({ viewType: "json", editorSavedValue: value }),
|
onCancel: () => setState({ viewType: "json" }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const path = `file:///${header}`;
|
const getSchemas = useCallback(async (): Promise<any[]> => {
|
||||||
const monaco = useMonaco();
|
const txObj = transactionsData.find(
|
||||||
|
td => td.TransactionType === currTxType
|
||||||
const getSchemas = useCallback((): any[] => {
|
);
|
||||||
const tt = selectedTransaction?.value;
|
|
||||||
const txObj = transactionsData.find(td => td.TransactionType === tt);
|
|
||||||
|
|
||||||
let genericSchemaProps: any;
|
let genericSchemaProps: any;
|
||||||
if (txObj) {
|
if (txObj) {
|
||||||
@@ -98,7 +103,6 @@ export const TxJson: FC<JsonProps> = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
uri: "file:///main-schema.json", // id of the first schema
|
uri: "file:///main-schema.json", // id of the first schema
|
||||||
@@ -130,6 +134,9 @@ export const TxJson: FC<JsonProps> = ({
|
|||||||
Amount: {
|
Amount: {
|
||||||
$ref: "file:///amount-schema.json",
|
$ref: "file:///amount-schema.json",
|
||||||
},
|
},
|
||||||
|
Fee: {
|
||||||
|
$ref: "file:///fee-schema.json",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -141,65 +148,85 @@ export const TxJson: FC<JsonProps> = ({
|
|||||||
enum: accounts.map(acc => acc.address),
|
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,
|
...amountSchema,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [accounts, header, selectedTransaction?.value]);
|
}, [accounts, currTxType, estimatedFee, header]);
|
||||||
|
|
||||||
|
const [monacoInst, setMonacoInst] = useState<typeof monaco>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!monaco) return;
|
if (!monacoInst) return;
|
||||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
getSchemas().then(schemas => {
|
||||||
validate: true,
|
monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
schemas: getSchemas(),
|
validate: true,
|
||||||
|
schemas,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, [getSchemas, monaco]);
|
}, [getSchemas, monacoInst]);
|
||||||
|
|
||||||
|
const hasUnsaved = useMemo(
|
||||||
|
() => editorValue !== getJsonString?.(),
|
||||||
|
[editorValue, getJsonString]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Monaco
|
||||||
fluid
|
rootProps={{
|
||||||
column
|
css: { height: "calc(100% - 45px)" },
|
||||||
css={{ height: "calc(100% - 45px)", position: "relative" }}
|
}}
|
||||||
>
|
language={"json"}
|
||||||
<Editor
|
id={header}
|
||||||
className="hooks-editor"
|
height="100%"
|
||||||
language={"json"}
|
value={editorValue}
|
||||||
path={path}
|
onChange={val => setState({ editorValue: val })}
|
||||||
height="100%"
|
onMount={(editor, monaco) => {
|
||||||
beforeMount={monaco => {
|
editor.updateOptions({
|
||||||
monaco.editor.defineTheme("dark", dark as any);
|
minimap: { enabled: false },
|
||||||
monaco.editor.defineTheme("light", light as any);
|
glyphMargin: true,
|
||||||
}}
|
tabSize: editorSettings.tabSize,
|
||||||
value={editorValue}
|
dragAndDrop: true,
|
||||||
onChange={val => setState({ editorValue: val })}
|
fontSize: 14,
|
||||||
onMount={(editor, monaco) => {
|
});
|
||||||
editor.updateOptions({
|
|
||||||
minimap: { enabled: false },
|
|
||||||
glyphMargin: true,
|
|
||||||
tabSize: editorSettings.tabSize,
|
|
||||||
dragAndDrop: true,
|
|
||||||
fontSize: 14,
|
|
||||||
});
|
|
||||||
|
|
||||||
// register onExit cb
|
setMonacoInst(monaco);
|
||||||
const model = editor.getModel();
|
// register onExit cb
|
||||||
model?.onWillDispose(() => onExit(model.getValue()));
|
const model = editor.getModel();
|
||||||
|
model?.onWillDispose(() => onExit(model.getValue()));
|
||||||
// set json defaults
|
}}
|
||||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
overlay={
|
||||||
validate: true,
|
hasUnsaved ? (
|
||||||
schemas: getSchemas(),
|
<Flex
|
||||||
});
|
row
|
||||||
}}
|
align="center"
|
||||||
theme={theme === "dark" ? "dark" : "light"}
|
css={{ fontSize: "$xs", color: "$textMuted", ml: "auto" }}
|
||||||
/>
|
>
|
||||||
{hasUnsaved && (
|
<Text muted small>
|
||||||
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
|
This file has unsaved changes.
|
||||||
This file has unsaved changes.{" "}
|
</Text>
|
||||||
<Link onClick={() => saveState(editorValue, txState)}>save</Link>{" "}
|
<Link
|
||||||
<Link onClick={discardChanges}>discard</Link>
|
css={{ ml: "$1" }}
|
||||||
</Text>
|
onClick={() => saveState(editorValue || "", currTxType)}
|
||||||
)}
|
>
|
||||||
</Flex>
|
save
|
||||||
|
</Link>
|
||||||
|
<Link css={{ ml: "$1" }} onClick={discardChanges}>
|
||||||
|
discard
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC } from "react";
|
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import Container from "../Container";
|
import Container from "../Container";
|
||||||
import Flex from "../Flex";
|
import Flex from "../Flex";
|
||||||
import Input from "../Input";
|
import Input from "../Input";
|
||||||
@@ -7,19 +7,30 @@ import Text from "../Text";
|
|||||||
import {
|
import {
|
||||||
SelectOption,
|
SelectOption,
|
||||||
TransactionState,
|
TransactionState,
|
||||||
transactionsData,
|
transactionsOptions,
|
||||||
TxFields,
|
TxFields,
|
||||||
|
getTxFields,
|
||||||
|
defaultTransactionType,
|
||||||
} from "../../state/transactions";
|
} from "../../state/transactions";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import state from "../../state";
|
import state from "../../state";
|
||||||
import { streamState } from "../DebugStream";
|
import { streamState } from "../DebugStream";
|
||||||
|
import { Button } from "..";
|
||||||
|
import Textarea from "../Textarea";
|
||||||
|
|
||||||
interface UIProps {
|
interface UIProps {
|
||||||
setState: (pTx?: Partial<TransactionState> | undefined) => void;
|
setState: (
|
||||||
|
pTx?: Partial<TransactionState> | undefined
|
||||||
|
) => TransactionState | undefined;
|
||||||
state: TransactionState;
|
state: TransactionState;
|
||||||
|
estimateFee?: (...arg: any) => Promise<string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
export const TxUI: FC<UIProps> = ({
|
||||||
|
state: txState,
|
||||||
|
setState,
|
||||||
|
estimateFee,
|
||||||
|
}) => {
|
||||||
const { accounts } = useSnapshot(state);
|
const { accounts } = useSnapshot(state);
|
||||||
const {
|
const {
|
||||||
selectedAccount,
|
selectedAccount,
|
||||||
@@ -28,11 +39,6 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
|||||||
txFields,
|
txFields,
|
||||||
} = txState;
|
} = txState;
|
||||||
|
|
||||||
const transactionsOptions = transactionsData.map(tx => ({
|
|
||||||
value: tx.TransactionType,
|
|
||||||
label: tx.TransactionType,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const accountOptions: SelectOption[] = accounts.map(acc => ({
|
const accountOptions: SelectOption[] = accounts.map(acc => ({
|
||||||
label: acc.name,
|
label: acc.name,
|
||||||
value: acc.address,
|
value: acc.address,
|
||||||
@@ -45,35 +51,84 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
|||||||
}))
|
}))
|
||||||
.filter(acc => acc.value !== selectedAccount?.value);
|
.filter(acc => acc.value !== selectedAccount?.value);
|
||||||
|
|
||||||
const resetOptions = (tt: string) => {
|
const [feeLoading, setFeeLoading] = useState(false);
|
||||||
const txFields: TxFields | undefined = transactionsData.find(
|
|
||||||
tx => tx.TransactionType === tt
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!txFields) return setState({ txFields: {} });
|
const resetFields = useCallback(
|
||||||
|
(tt: string) => {
|
||||||
|
const fields = getTxFields(tt);
|
||||||
|
|
||||||
const _txFields = Object.keys(txFields)
|
if (fields.Destination !== undefined) {
|
||||||
.filter(key => !["TransactionType", "Account", "Sequence"].includes(key))
|
fields.Destination = "";
|
||||||
.reduce<TxFields>(
|
} else {
|
||||||
(tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf),
|
fields.Destination = undefined;
|
||||||
{}
|
}
|
||||||
);
|
return setState({ txFields: fields, selectedDestAccount: null });
|
||||||
|
},
|
||||||
if (!_txFields.Destination) setState({ selectedDestAccount: null });
|
[setState]
|
||||||
setState({ txFields: _txFields });
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetAccount = (acc: SelectOption) => {
|
const handleSetAccount = (acc: SelectOption) => {
|
||||||
setState({ selectedAccount: acc });
|
setState({ selectedAccount: acc });
|
||||||
streamState.selectedAccount = acc;
|
streamState.selectedAccount = acc;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeTxType = (tt: SelectOption) => {
|
const handleSetField = useCallback(
|
||||||
setState({ selectedTransaction: tt });
|
(field: keyof TxFields, value: string, opFields?: TxFields) => {
|
||||||
resetOptions(tt.value);
|
const fields = opFields || txFields;
|
||||||
};
|
const obj = fields[field];
|
||||||
|
setState({
|
||||||
|
txFields: {
|
||||||
|
...fields,
|
||||||
|
[field]: typeof obj === "object" ? { ...obj, $value: value } : value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setState, txFields]
|
||||||
|
);
|
||||||
|
|
||||||
const specialFields = ["TransactionType", "Account", "Destination"];
|
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 = resetFields(tt.value);
|
||||||
|
|
||||||
|
handleEstimateFee(newState, true);
|
||||||
|
},
|
||||||
|
[handleEstimateFee, resetFields, 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 specialFields = ["TransactionType", "Account"];
|
||||||
|
if (fields.Destination !== undefined) {
|
||||||
|
specialFields.push("Destination");
|
||||||
|
}
|
||||||
|
|
||||||
const otherFields = Object.keys(txFields).filter(
|
const otherFields = Object.keys(txFields).filter(
|
||||||
k => !specialFields.includes(k)
|
k => !specialFields.includes(k)
|
||||||
@@ -87,7 +142,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
|||||||
height: "calc(100% - 45px)",
|
height: "calc(100% - 45px)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
|
<Flex column fluid css={{ height: "100%", overflowY: "auto", pr: "$1" }}>
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
fluid
|
fluid
|
||||||
@@ -134,7 +189,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
|||||||
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
|
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
{txFields.Destination !== undefined && (
|
{fields.Destination !== undefined && (
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
fluid
|
fluid
|
||||||
@@ -165,7 +220,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
|||||||
let value: string | undefined;
|
let value: string | undefined;
|
||||||
if (typeof _value === "object") {
|
if (typeof _value === "object") {
|
||||||
if (_value.$type === "json" && typeof _value.$value === "object") {
|
if (_value.$type === "json" && typeof _value.$value === "object") {
|
||||||
value = JSON.stringify(_value.$value);
|
value = JSON.stringify(_value.$value, null, 2);
|
||||||
} else {
|
} else {
|
||||||
value = _value.$value.toString();
|
value = _value.$value.toString();
|
||||||
}
|
}
|
||||||
@@ -173,37 +228,99 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
|||||||
value = _value?.toString();
|
value = _value?.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
let isXrp = typeof _value === "object" && _value.$type === "xrp";
|
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 (
|
return (
|
||||||
<Flex
|
<Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
|
||||||
key={field}
|
<Flex
|
||||||
row
|
row
|
||||||
fluid
|
fluid
|
||||||
css={{
|
css={{
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
mb: "$3",
|
position: "relative",
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
{field + (isXrp ? " (XRP)" : "")}:{" "}
|
|
||||||
</Text>
|
|
||||||
<Input
|
|
||||||
value={value}
|
|
||||||
onChange={e => {
|
|
||||||
setState({
|
|
||||||
txFields: {
|
|
||||||
...txFields,
|
|
||||||
[field]:
|
|
||||||
typeof _value === "object"
|
|
||||||
? { ..._value, $value: e.target.value }
|
|
||||||
: e.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
css={{ width: "70%", flex: "inherit" }}
|
>
|
||||||
/>
|
<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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
40
components/icons/Carbon.tsx
Normal file
40
components/icons/Carbon.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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;
|
||||||
75
components/icons/Firewall.tsx
Normal file
75
components/icons/Firewall.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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;
|
||||||
40
components/icons/Notary.tsx
Normal file
40
components/icons/Notary.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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;
|
||||||
61
components/icons/Peggy.tsx
Normal file
61
components/icons/Peggy.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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;
|
||||||
40
components/icons/Starter.tsx
Normal file
40
components/icons/Starter.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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;
|
||||||
@@ -12,6 +12,5 @@ export { default as Box } from "./Box";
|
|||||||
export { default as Button } from "./Button";
|
export { default as Button } from "./Button";
|
||||||
export { default as Pre } from "./Pre";
|
export { default as Pre } from "./Pre";
|
||||||
export { default as ButtonGroup } from "./ButtonGroup";
|
export { default as ButtonGroup } from "./ButtonGroup";
|
||||||
export { default as DeployFooter } from "./DeployFooter";
|
|
||||||
export * from "./Dialog";
|
export * from "./Dialog";
|
||||||
export * from "./DropdownMenu";
|
export * from "./DropdownMenu";
|
||||||
|
|||||||
@@ -40,9 +40,9 @@
|
|||||||
{
|
{
|
||||||
"label": "Token",
|
"label": "Token",
|
||||||
"body": {
|
"body": {
|
||||||
"currency": "${1:13.1}",
|
"currency": "${1:USD}",
|
||||||
"value": "${2:FOO}",
|
"value": "${2:100}",
|
||||||
"description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
|
"issuer": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -55,7 +55,8 @@
|
|||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"TransactionType": "EscrowCancel",
|
"TransactionType": "EscrowCancel",
|
||||||
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"OfferSequence": 7
|
"OfferSequence": 7,
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
@@ -69,7 +70,8 @@
|
|||||||
"FinishAfter": 533171558,
|
"FinishAfter": 533171558,
|
||||||
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
|
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
|
||||||
"DestinationTag": 23480,
|
"DestinationTag": 23480,
|
||||||
"SourceTag": 11747
|
"SourceTag": 11747,
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
@@ -77,32 +79,50 @@
|
|||||||
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"OfferSequence": 7,
|
"OfferSequence": 7,
|
||||||
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
|
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
|
||||||
"Fulfillment": "A0028000"
|
"Fulfillment": "A0028000",
|
||||||
|
"Fee": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TransactionType": "NFTokenMint",
|
||||||
|
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
|
||||||
|
"Fee": "10",
|
||||||
|
"NFTokenTaxon": 0,
|
||||||
|
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "NFTokenBurn",
|
"TransactionType": "NFTokenBurn",
|
||||||
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
|
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
|
||||||
"Fee": "10",
|
"Fee": "10",
|
||||||
"TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
|
"NFTokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "NFTokenAcceptOffer",
|
"TransactionType": "NFTokenAcceptOffer",
|
||||||
"Fee": "10"
|
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
|
||||||
|
"Fee": "10",
|
||||||
|
"NFTokenSellOffer": "A2FA1A9911FE2AEF83DAB05F437768E26A301EF899BD31EB85E704B3D528FF18",
|
||||||
|
"NFTokenBuyOffer": "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862",
|
||||||
|
"NFTokenBrokerFee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "NFTokenCancelOffer",
|
"TransactionType": "NFTokenCancelOffer",
|
||||||
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
||||||
"TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
|
"Fee": "10",
|
||||||
|
"NFTokenOffers": {
|
||||||
|
"$type": "json",
|
||||||
|
"$value": ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "NFTokenCreateOffer",
|
"TransactionType": "NFTokenCreateOffer",
|
||||||
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
|
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
|
||||||
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
|
"NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"$value": "100",
|
"$value": "100",
|
||||||
"$type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Flags": 1
|
"Flags": 1,
|
||||||
|
"Destination": "",
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "OfferCancel",
|
"TransactionType": "OfferCancel",
|
||||||
@@ -150,7 +170,8 @@
|
|||||||
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
|
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
|
||||||
"CancelAfter": 533171558,
|
"CancelAfter": 533171558,
|
||||||
"DestinationTag": 23480,
|
"DestinationTag": 23480,
|
||||||
"SourceTag": 11747
|
"SourceTag": 11747,
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
@@ -160,7 +181,8 @@
|
|||||||
"$value": "200",
|
"$value": "200",
|
||||||
"$type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Expiration": 543171558
|
"Expiration": 543171558,
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Flags": 0,
|
"Flags": 0,
|
||||||
@@ -212,9 +234,13 @@
|
|||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
"Flags": 262144,
|
"Flags": 262144,
|
||||||
"LastLedgerSequence": 8007750,
|
"LastLedgerSequence": 8007750,
|
||||||
"Amount": {
|
"LimitAmount": {
|
||||||
"$value": "100",
|
"$type": "json",
|
||||||
"$type": "xrp"
|
"$value": {
|
||||||
|
"currency": "USD",
|
||||||
|
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
|
||||||
|
"value": "100"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Sequence": 12
|
"Sequence": 12
|
||||||
}
|
}
|
||||||
|
|||||||
12057
package-lock.json
generated
12057
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -12,19 +12,21 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codingame/monaco-jsonrpc": "^0.3.1",
|
"@codingame/monaco-jsonrpc": "^0.3.1",
|
||||||
"@codingame/monaco-languageclient": "^0.17.0",
|
"@codingame/monaco-languageclient": "^0.17.0",
|
||||||
"@monaco-editor/react": "^4.4.1",
|
"@monaco-editor/react": "^4.4.5",
|
||||||
"@octokit/core": "^3.5.1",
|
"@octokit/core": "^3.5.1",
|
||||||
"@radix-ui/colors": "^0.1.7",
|
"@radix-ui/colors": "^0.1.7",
|
||||||
"@radix-ui/react-alert-dialog": "^0.1.1",
|
"@radix-ui/react-alert-dialog": "^0.1.1",
|
||||||
|
"@radix-ui/react-context-menu": "^0.1.6",
|
||||||
"@radix-ui/react-dialog": "^0.1.1",
|
"@radix-ui/react-dialog": "^0.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.1.1",
|
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||||
"@radix-ui/react-id": "^0.1.1",
|
"@radix-ui/react-id": "^0.1.1",
|
||||||
"@radix-ui/react-label": "^0.1.5",
|
"@radix-ui/react-label": "^0.1.5",
|
||||||
"@radix-ui/react-popover": "^0.1.6",
|
"@radix-ui/react-popover": "^0.1.6",
|
||||||
"@radix-ui/react-switch": "^0.1.5",
|
"@radix-ui/react-switch": "^0.1.5",
|
||||||
"@radix-ui/react-tooltip": "^0.1.7",
|
"@radix-ui/react-tooltip": "^0.1.7",
|
||||||
"@stitches/react": "^1.2.6-0",
|
"@stitches/react": "^1.2.8",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
|
"comment-parser": "^1.3.1",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "^8.0.7",
|
"filesize": "^8.0.7",
|
||||||
@@ -34,7 +36,8 @@
|
|||||||
"lodash.xor": "^4.5.0",
|
"lodash.xor": "^4.5.0",
|
||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"next": "^12.0.4",
|
"next": "^12.0.4",
|
||||||
"next-auth": "^4.0.0-beta.5",
|
"next-auth": "^4.10.1",
|
||||||
|
"next-plausible": "^3.2.0",
|
||||||
"next-themes": "^0.1.1",
|
"next-themes": "^0.1.1",
|
||||||
"normalize-url": "^7.0.2",
|
"normalize-url": "^7.0.2",
|
||||||
"octokit": "^1.7.0",
|
"octokit": "^1.7.0",
|
||||||
@@ -59,7 +62,7 @@
|
|||||||
"vscode-languageserver": "^7.0.0",
|
"vscode-languageserver": "^7.0.0",
|
||||||
"vscode-uri": "^3.0.2",
|
"vscode-uri": "^3.0.2",
|
||||||
"wabt": "1.0.16",
|
"wabt": "1.0.16",
|
||||||
"xrpl-accountlib": "^1.3.2",
|
"xrpl-accountlib": "^1.5.2",
|
||||||
"xrpl-client": "^1.9.4"
|
"xrpl-client": "^1.9.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -73,5 +76,8 @@
|
|||||||
"eslint-config-next": "11.1.2",
|
"eslint-config-next": "11.1.2",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"typescript": "4.4.4"
|
"typescript": "4.4.4"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"ripple-binary-codec": "=1.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes";
|
|||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { IdProvider } from "@radix-ui/react-id";
|
import { IdProvider } from "@radix-ui/react-id";
|
||||||
|
import PlausibleProvider from "next-plausible";
|
||||||
|
|
||||||
import { darkTheme, css } from "../stitches.config";
|
import { darkTheme, css } from "../stitches.config";
|
||||||
import Navigation from "../components/Navigation";
|
import Navigation from "../components/Navigation";
|
||||||
@@ -16,7 +17,9 @@ import state from "../state";
|
|||||||
import TimeAgo from "javascript-time-ago";
|
import TimeAgo from "javascript-time-ago";
|
||||||
import en from "javascript-time-ago/locale/en.json";
|
import en from "javascript-time-ago/locale/en.json";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import Alert from '../components/AlertDialog';
|
import Alert from "../components/AlertDialog";
|
||||||
|
import { Button, Flex } from "../components";
|
||||||
|
import { ChatCircleText } from "phosphor-react";
|
||||||
|
|
||||||
TimeAgo.setDefaultLocale(en.locale);
|
TimeAgo.setDefaultLocale(en.locale);
|
||||||
TimeAgo.addLocale(en);
|
TimeAgo.addLocale(en);
|
||||||
@@ -37,7 +40,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
|||||||
if (
|
if (
|
||||||
!gistId &&
|
!gistId &&
|
||||||
router.isReady &&
|
router.isReady &&
|
||||||
!router.pathname.includes("/sign-in") &&
|
router.pathname.includes("/develop") &&
|
||||||
!snap.files.length &&
|
!snap.files.length &&
|
||||||
!snap.mainModalShowed
|
!snap.mainModalShowed
|
||||||
) {
|
) {
|
||||||
@@ -61,22 +64,22 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
|||||||
<meta name="format-detection" content="telephone=no" />
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<meta property="og:url" content={`${origin}${router.asPath}`} />
|
<meta property="og:url" content={`${origin}${router.asPath}`} />
|
||||||
|
|
||||||
<title>XRPL Hooks Editor</title>
|
<title>XRPL Hooks Builder</title>
|
||||||
<meta property="og:title" content="XRPL Hooks Editor" />
|
<meta property="og:title" content="XRPL Hooks Builder" />
|
||||||
<meta name="twitter:title" content="XRPL Hooks Editor" />
|
<meta name="twitter:title" content="XRPL Hooks Builder" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:site" content="@xrpllabs" />
|
<meta name="twitter:site" content="@XRPLF" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
|
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
|
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="twitter:description"
|
name="twitter:description"
|
||||||
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger.."
|
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
|
||||||
/>
|
/>
|
||||||
<meta property="og:image" content={`${origin}${shareImg}`} />
|
<meta property="og:image" content={`${origin}${shareImg}`} />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
@@ -101,7 +104,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
|||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
|
||||||
<meta name="application-name" content="XRPL Hooks Editor" />
|
<meta name="application-name" content="XRPL Hooks Builder" />
|
||||||
<meta name="msapplication-TileColor" content="#c10ad0" />
|
<meta name="msapplication-TileColor" content="#c10ad0" />
|
||||||
<meta
|
<meta
|
||||||
name="theme-color"
|
name="theme-color"
|
||||||
@@ -114,6 +117,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
|||||||
media="(prefers-color-scheme: light)"
|
media="(prefers-color-scheme: light)"
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<IdProvider>
|
<IdProvider>
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
@@ -125,23 +129,40 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
|||||||
dark: darkTheme.className,
|
dark: darkTheme.className,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Navigation />
|
<PlausibleProvider
|
||||||
<Component {...pageProps} />
|
domain="hooks-builder.xrpl.org"
|
||||||
<Toaster
|
trackOutboundLinks
|
||||||
toastOptions={{
|
>
|
||||||
className: css({
|
<Navigation />
|
||||||
backgroundColor: "$mauve1",
|
<Component {...pageProps} />
|
||||||
color: "$mauve10",
|
<Toaster
|
||||||
fontSize: "$sm",
|
toastOptions={{
|
||||||
zIndex: 9999,
|
className: css({
|
||||||
".dark &": {
|
backgroundColor: "$mauve1",
|
||||||
backgroundColor: "$mauve4",
|
color: "$mauve10",
|
||||||
color: "$mauve12",
|
fontSize: "$sm",
|
||||||
},
|
zIndex: 9999,
|
||||||
})(),
|
".dark &": {
|
||||||
}}
|
backgroundColor: "$mauve4",
|
||||||
/>
|
color: "$mauve12",
|
||||||
<Alert />
|
},
|
||||||
|
})(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Alert />
|
||||||
|
<Flex
|
||||||
|
as="a"
|
||||||
|
href="https://github.com/XRPLF/Hooks/discussions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
css={{ position: "fixed", right: "$4", bottom: "$4" }}
|
||||||
|
>
|
||||||
|
<Button size="sm" variant="primary" outline>
|
||||||
|
<ChatCircleText size={14} style={{ marginRight: "0px" }} />
|
||||||
|
Bugs & Discussions
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</PlausibleProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</IdProvider>
|
</IdProvider>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Label } from "@radix-ui/react-label";
|
import { Label } from "@radix-ui/react-label";
|
||||||
import { Switch, SwitchThumb } from "../../components/Switch";
|
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Gear, Play } from "phosphor-react";
|
import { FileJs, Gear, Play } from "phosphor-react";
|
||||||
import Hotkeys from "react-hot-keys";
|
import Hotkeys from "react-hot-keys";
|
||||||
import Split from "react-split";
|
import Split from "react-split";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
@@ -10,6 +9,7 @@ import { ButtonGroup, Flex } from "../../components";
|
|||||||
import Box from "../../components/Box";
|
import Box from "../../components/Box";
|
||||||
import Button from "../../components/Button";
|
import Button from "../../components/Button";
|
||||||
import Popover from "../../components/Popover";
|
import Popover from "../../components/Popover";
|
||||||
|
import RunScript from "../../components/RunScript";
|
||||||
import state from "../../state";
|
import state from "../../state";
|
||||||
import { compileCode } from "../../state/actions";
|
import { compileCode } from "../../state/actions";
|
||||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
||||||
@@ -141,59 +141,6 @@ const CompilerSettings = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Box>
|
</Box>
|
||||||
<Box css={{ flexDirection: "column" }}>
|
|
||||||
<Label
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clean WASM (experimental){" "}
|
|
||||||
<Popover
|
|
||||||
css={{
|
|
||||||
maxWidth: "240px",
|
|
||||||
lineHeight: "1.3",
|
|
||||||
a: {
|
|
||||||
color: "$purple11",
|
|
||||||
},
|
|
||||||
".dark &": {
|
|
||||||
backgroundColor: "$black !important",
|
|
||||||
|
|
||||||
".arrow": {
|
|
||||||
fill: "$colors$black",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
content="Cleaner removes unwanted compiler-provided exports and functions from a wasm binary to make it (more) suitable for being used as a Hook"
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
css={{
|
|
||||||
position: "relative",
|
|
||||||
top: "-1px",
|
|
||||||
mx: "$1",
|
|
||||||
backgroundColor: "$mauve8",
|
|
||||||
borderRadius: "$full",
|
|
||||||
cursor: "pointer",
|
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
?
|
|
||||||
</Flex>
|
|
||||||
</Popover>
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
css={{ mt: "$2" }}
|
|
||||||
checked={snap.compileOptions.strip}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
state.compileOptions.strip = checked;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SwitchThumb />
|
|
||||||
</Switch>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -213,7 +160,7 @@ const Home: NextPage = () => {
|
|||||||
>
|
>
|
||||||
<main style={{ display: "flex", flex: 1, position: "relative" }}>
|
<main style={{ display: "flex", flex: 1, position: "relative" }}>
|
||||||
<HooksEditor />
|
<HooksEditor />
|
||||||
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
|
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
|
||||||
"c" && (
|
"c" && (
|
||||||
<Hotkeys
|
<Hotkeys
|
||||||
keyName="command+b,ctrl+b"
|
keyName="command+b,ctrl+b"
|
||||||
@@ -250,20 +197,61 @@ const Home: NextPage = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Hotkeys>
|
</Hotkeys>
|
||||||
)}
|
)}
|
||||||
|
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
|
||||||
|
"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={snap.files[snap.active]} />
|
||||||
|
</Flex>
|
||||||
|
</Hotkeys>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
<Box
|
<Flex css={{ width: "100%" }}>
|
||||||
css={{
|
<Flex
|
||||||
display: "flex",
|
css={{
|
||||||
background: "$mauve1",
|
flex: 1,
|
||||||
position: "relative",
|
background: "$mauve1",
|
||||||
}}
|
position: "relative",
|
||||||
>
|
borderRight: "1px solid $mauve8",
|
||||||
<LogBox
|
}}
|
||||||
title="Development Log"
|
>
|
||||||
clearLog={() => (state.logs = [])}
|
<LogBox
|
||||||
logs={snap.logs}
|
title="Development Log"
|
||||||
/>
|
clearLog={() => (state.logs = [])}
|
||||||
</Box>
|
logs={snap.logs}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
|
||||||
|
"js" && (
|
||||||
|
<Flex
|
||||||
|
css={{
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogBox
|
||||||
|
Icon={FileJs}
|
||||||
|
title="Script Log"
|
||||||
|
logs={snap.scriptLogs}
|
||||||
|
clearLog={() => (state.scriptLogs = [])}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</Split>
|
</Split>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import Split from "react-split";
|
|||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import { Box, Container, Flex, Tab, Tabs } from "../../components";
|
import { Box, Container, Flex, Tab, Tabs } from "../../components";
|
||||||
import Transaction from "../../components/Transaction";
|
import Transaction from "../../components/Transaction";
|
||||||
import state from "../../state";
|
import state, { renameTxState } from "../../state";
|
||||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
||||||
import { transactionsState, modifyTransaction } from "../../state";
|
import { transactionsState, modifyTxState } from "../../state";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FileJs } from "phosphor-react";
|
||||||
|
import RunScript from "../../components/RunScript";
|
||||||
|
|
||||||
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
|
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -19,14 +22,42 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
|
// This and useEffect is here to prevent useLayoutEffect warnings from react-split
|
||||||
|
const [showComponent, setShowComponent] = useState(false);
|
||||||
const { transactionLogs } = useSnapshot(state);
|
const { transactionLogs } = useSnapshot(state);
|
||||||
const { transactions, activeHeader } = useSnapshot(transactionsState);
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container css={{ px: 0 }}>
|
<Container css={{ px: 0 }}>
|
||||||
<Split
|
<Split
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
sizes={getSplit("testVertical") || [50, 50]}
|
sizes={
|
||||||
|
hasScripts && getSplit("testVertical")?.length === 2
|
||||||
|
? [50, 20, 30]
|
||||||
|
: hasScripts
|
||||||
|
? [50, 20, 50]
|
||||||
|
: [50, 50]
|
||||||
|
}
|
||||||
gutterSize={4}
|
gutterSize={4}
|
||||||
gutterAlign="center"
|
gutterAlign="center"
|
||||||
style={{ height: "calc(100vh - 60px)" }}
|
style={{ height: "calc(100vh - 60px)" }}
|
||||||
@@ -56,25 +87,26 @@ const Test = () => {
|
|||||||
>
|
>
|
||||||
<Box css={{ width: "55%", px: "$2" }}>
|
<Box css={{ width: "55%", px: "$2" }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
label="Transaction"
|
||||||
activeHeader={activeHeader}
|
activeHeader={activeHeader}
|
||||||
// TODO make header a required field
|
// TODO make header a required field
|
||||||
onChangeActive={(idx, header) => {
|
onChangeActive={(idx, header) => {
|
||||||
if (header) transactionsState.activeHeader = header;
|
if (header) transactionsState.activeHeader = header;
|
||||||
}}
|
}}
|
||||||
keepAllAlive
|
keepAllAlive
|
||||||
forceDefaultExtension
|
defaultExtension="json"
|
||||||
defaultExtension=".json"
|
allowedExtensions={["json"]}
|
||||||
onCreateNewTab={header => modifyTransaction(header, {})}
|
onCreateNewTab={header => modifyTxState(header, {})}
|
||||||
|
onRenameTab={(idx, nwName, oldName = "") =>
|
||||||
|
renameTxState(oldName, nwName)
|
||||||
|
}
|
||||||
onCloseTab={(idx, header) =>
|
onCloseTab={(idx, header) =>
|
||||||
header && modifyTransaction(header, undefined)
|
header && modifyTxState(header, undefined)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{transactions.map(({ header, state }) => (
|
{transactions.map(({ header, state }) => (
|
||||||
<Tab key={header} header={header}>
|
<Tab key={header} header={header}>
|
||||||
<Transaction
|
<Transaction state={state} header={header} />
|
||||||
state={state}
|
|
||||||
header={header}
|
|
||||||
/>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -84,8 +116,25 @@ const Test = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Split>
|
</Split>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{hasScripts ? (
|
||||||
<Flex row fluid>
|
<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>
|
||||||
<Split
|
<Split
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
sizes={[50, 50]}
|
sizes={[50, 50]}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,41 +27,35 @@ export const names = [
|
|||||||
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
|
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
|
||||||
* is protected with CORS so that's why we did our own endpoint
|
* is protected with CORS so that's why we did our own endpoint
|
||||||
*/
|
*/
|
||||||
export const addFaucetAccount = async (showToast: boolean = false) => {
|
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
|
||||||
// Lets limit the number of faucet accounts to 5 for now
|
if (typeof window === undefined) return
|
||||||
if (state.accounts.length > 5) {
|
|
||||||
return toast.error("You can only have maximum 6 accounts");
|
|
||||||
}
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
|
|
||||||
|
const toastId = showToast ? toast.loading("Creating account") : "";
|
||||||
const toastId = showToast ? toast.loading("Creating account") : "";
|
const res = await fetch(`${window.location.origin}/api/faucet`, {
|
||||||
const res = await fetch(`${window.location.origin}/api/faucet`, {
|
method: "POST",
|
||||||
method: "POST",
|
});
|
||||||
});
|
const json: FaucetAccountRes | { error: string } = await res.json();
|
||||||
const json: FaucetAccountRes | { error: string } = await res.json();
|
if ("error" in json) {
|
||||||
if ("error" in json) {
|
if (showToast) {
|
||||||
if (showToast) {
|
return toast.error(json.error, { id: toastId });
|
||||||
return toast.error(json.error, { id: toastId });
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (showToast) {
|
return;
|
||||||
toast.success("New account created", { id: toastId });
|
|
||||||
}
|
|
||||||
const currNames = state.accounts.map(acc => acc.name);
|
|
||||||
state.accounts.push({
|
|
||||||
name: names.filter(name => !currNames.includes(name))[0],
|
|
||||||
xrp: (json.xrp || 0 * 1000000).toString(),
|
|
||||||
address: json.address,
|
|
||||||
secret: json.secret,
|
|
||||||
sequence: 1,
|
|
||||||
hooks: [],
|
|
||||||
isLoading: false,
|
|
||||||
version: '2'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (showToast) {
|
||||||
|
toast.success("New account created", { id: toastId });
|
||||||
|
}
|
||||||
|
const currNames = state.accounts.map(acc => acc.name);
|
||||||
|
state.accounts.push({
|
||||||
|
name: name || names.filter(name => !currNames.includes(name))[0],
|
||||||
|
xrp: (json.xrp || 0 * 1000000).toString(),
|
||||||
|
address: json.address,
|
||||||
|
secret: json.secret,
|
||||||
|
sequence: 1,
|
||||||
|
hooks: [],
|
||||||
|
isLoading: false,
|
||||||
|
version: '2'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,80 +14,113 @@ import { ref } from "valtio";
|
|||||||
*/
|
*/
|
||||||
export const compileCode = async (activeId: number) => {
|
export const compileCode = async (activeId: number) => {
|
||||||
// Save the file to global state
|
// Save the file to global state
|
||||||
saveFile(false);
|
saveFile(false, activeId);
|
||||||
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
|
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
|
||||||
throw Error("Missing env!");
|
throw Error("Missing env!");
|
||||||
}
|
}
|
||||||
// Bail out if we're already compiling
|
// Bail out if we're already compiling
|
||||||
if (state.compiling) {
|
if (state.compiling) {
|
||||||
// if compiling is ongoing return
|
// if compiling is ongoing return // TODO Inform user about it.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Set loading state to true
|
// Set loading state to true
|
||||||
state.compiling = true;
|
state.compiling = true;
|
||||||
state.logs = []
|
state.logs = []
|
||||||
|
const file = state.files[activeId]
|
||||||
try {
|
try {
|
||||||
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
|
file.containsErrors = false
|
||||||
method: "POST",
|
let res: Response
|
||||||
headers: {
|
try {
|
||||||
"Content-Type": "application/json",
|
res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
|
||||||
},
|
method: "POST",
|
||||||
body: JSON.stringify({
|
headers: {
|
||||||
output: "wasm",
|
"Content-Type": "application/json",
|
||||||
compress: true,
|
},
|
||||||
strip: state.compileOptions.strip,
|
body: JSON.stringify({
|
||||||
files: [
|
output: "wasm",
|
||||||
{
|
compress: true,
|
||||||
type: "c",
|
strip: state.compileOptions.strip,
|
||||||
options: state.compileOptions.optimizationLevel || '-O0',
|
files: [
|
||||||
name: state.files[activeId].name,
|
{
|
||||||
src: state.files[activeId].content,
|
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();
|
const json = await res.json();
|
||||||
state.compiling = false;
|
state.compiling = false;
|
||||||
if (!json.success) {
|
if (!json.success) {
|
||||||
state.logs.push({ type: "error", message: json.message });
|
const errors = [json.message]
|
||||||
if (json.tasks && json.tasks.length > 0) {
|
if (json.tasks && json.tasks.length > 0) {
|
||||||
json.tasks.forEach((task: any) => {
|
json.tasks.forEach((task: any) => {
|
||||||
if (!task.success) {
|
if (!task.success) {
|
||||||
state.logs.push({ type: "error", message: task?.console });
|
errors.push(task?.console)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return toast.error(`Couldn't compile!`, { position: "bottom-center" });
|
throw errors
|
||||||
}
|
}
|
||||||
state.logs.push({
|
try {
|
||||||
type: "success",
|
// Decode base64 encoded wasm that is coming back from the endpoint
|
||||||
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
|
const bufferData = await decodeBinary(json.output);
|
||||||
link: Router.asPath.replace("develop", "deploy"),
|
|
||||||
linkText: "Go to deploy",
|
// Import wabt from and create human readable version of wasm file and
|
||||||
});
|
// put it into state
|
||||||
// Decode base64 encoded wasm that is coming back from the endpoint
|
const ww = (await import('wabt')).default()
|
||||||
const bufferData = await decodeBinary(json.output);
|
|
||||||
state.files[state.active].compiledContent = ref(bufferData);
|
|
||||||
state.files[state.active].lastCompiled = new Date();
|
|
||||||
// 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), {
|
const myModule = ww.readWasm(new Uint8Array(bufferData), {
|
||||||
readDebugNames: true,
|
readDebugNames: true,
|
||||||
});
|
});
|
||||||
myModule.applyNames();
|
myModule.applyNames();
|
||||||
|
|
||||||
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
|
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
|
||||||
state.files[state.active].compiledWatContent = wast;
|
|
||||||
toast.success("Compiled successfully!", { position: "bottom-center" });
|
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!")
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
state.logs.push({
|
|
||||||
type: "error",
|
if (err instanceof Array && typeof err[0] === 'string') {
|
||||||
message: "Error occured while compiling!",
|
err.forEach(message => {
|
||||||
});
|
state.logs.push({
|
||||||
|
type: "error",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else 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;
|
state.compiling = false;
|
||||||
|
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
|
||||||
|
file.containsErrors = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,4 +14,11 @@ export const createNewFile = (name: string) => {
|
|||||||
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
|
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
|
||||||
state.files.push(emptyFile);
|
state.files.push(emptyFile);
|
||||||
state.active = state.files.length - 1;
|
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}`)
|
||||||
|
|
||||||
|
file.name = nwName
|
||||||
};
|
};
|
||||||
@@ -3,16 +3,17 @@ import toast from "react-hot-toast";
|
|||||||
|
|
||||||
import state, { IAccount } from "../index";
|
import state, { IAccount } from "../index";
|
||||||
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
|
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
|
||||||
import { SetHookData } from "../../components/SetHookDialog";
|
|
||||||
import { Link } from "../../components";
|
import { Link } from "../../components";
|
||||||
import { ref } from "valtio";
|
import { ref } from "valtio";
|
||||||
|
import estimateFee from "../../utils/estimateFee";
|
||||||
|
import { SetHookData } from '../../utils/setHook';
|
||||||
|
|
||||||
export const sha256 = async (string: string) => {
|
export const sha256 = async (string: string) => {
|
||||||
const utf8 = new TextEncoder().encode(string);
|
const utf8 = new TextEncoder().encode(string);
|
||||||
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
|
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
const hashHex = hashArray
|
const hashHex = hashArray
|
||||||
.map(bytes => bytes.toString(16).padStart(2, "0"))
|
.map((bytes) => bytes.toString(16).padStart(2, "0"))
|
||||||
.join("");
|
.join("");
|
||||||
return hashHex;
|
return hashHex;
|
||||||
};
|
};
|
||||||
@@ -49,35 +50,31 @@ function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* deployHook function turns the wasm binary into
|
export const prepareDeployHookTx = async (
|
||||||
* hex string, signs the transaction and deploys it to
|
|
||||||
* Hooks testnet.
|
|
||||||
*/
|
|
||||||
export const deployHook = async (
|
|
||||||
account: IAccount & { name?: string },
|
account: IAccount & { name?: string },
|
||||||
data: SetHookData
|
data: SetHookData
|
||||||
) => {
|
) => {
|
||||||
if (
|
const activeFile = state.files[state.active]?.compiledContent
|
||||||
!state.files ||
|
? state.files[state.active]
|
||||||
state.files.length === 0 ||
|
: state.files.filter((file) => file.compiledContent)[0];
|
||||||
!state.files?.[state.active]?.compiledContent
|
|
||||||
) {
|
if (!state.files || state.files.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.files?.[state.active]?.compiledContent) {
|
if (!activeFile?.compiledContent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!state.client) {
|
if (!state.client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
|
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
|
||||||
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value);
|
const hookOnValues: (keyof TTS)[] = data.Invoke.map((tt) => tt.value);
|
||||||
const { HookParameters } = data;
|
const { HookParameters } = data;
|
||||||
const filteredHookParameters = HookParameters.filter(
|
const filteredHookParameters = HookParameters.filter(
|
||||||
hp =>
|
(hp) =>
|
||||||
hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
|
hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
|
||||||
)?.map(aa => ({
|
)?.map((aa) => ({
|
||||||
HookParameter: {
|
HookParameter: {
|
||||||
HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
|
HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
|
||||||
HookParameterValue: aa.HookParameter.HookParameterValue || "",
|
HookParameterValue: aa.HookParameter.HookParameterValue || "",
|
||||||
@@ -92,18 +89,17 @@ export const deployHook = async (
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const tx = {
|
const tx = {
|
||||||
Account: account.address,
|
Account: account.address,
|
||||||
TransactionType: "SetHook",
|
TransactionType: "SetHook",
|
||||||
Sequence: account.sequence,
|
Sequence: account.sequence,
|
||||||
Fee: "100000",
|
Fee: data.Fee,
|
||||||
Hooks: [
|
Hooks: [
|
||||||
{
|
{
|
||||||
Hook: {
|
Hook: {
|
||||||
CreateCode: arrayBufferToHex(
|
CreateCode: arrayBufferToHex(
|
||||||
state.files?.[state.active]?.compiledContent
|
activeFile?.compiledContent
|
||||||
).toUpperCase(),
|
).toUpperCase(),
|
||||||
HookOn: calculateHookOn(hookOnValues),
|
HookOn: calculateHookOn(hookOnValues),
|
||||||
HookNamespace,
|
HookNamespace,
|
||||||
@@ -117,18 +113,43 @@ export const deployHook = async (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (!state.client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const keypair = derive.familySeed(account.secret);
|
const keypair = derive.familySeed(account.secret);
|
||||||
|
|
||||||
const { signedTransaction } = sign(tx, keypair);
|
const { signedTransaction } = sign(tx, keypair);
|
||||||
const currentAccount = state.accounts.find(
|
const currentAccount = state.accounts.find(
|
||||||
acc => acc.address === account.address
|
(acc) => acc.address === account.address
|
||||||
);
|
);
|
||||||
if (currentAccount) {
|
if (currentAccount) {
|
||||||
currentAccount.isLoading = true;
|
currentAccount.isLoading = true;
|
||||||
}
|
}
|
||||||
let submitRes;
|
let submitRes;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
submitRes = await state.client.send({
|
submitRes = await state.client?.send({
|
||||||
command: "submit",
|
command: "submit",
|
||||||
tx_blob: signedTransaction,
|
tx_blob: signedTransaction,
|
||||||
});
|
});
|
||||||
@@ -143,14 +164,14 @@ export const deployHook = async (
|
|||||||
message: ref(
|
message: ref(
|
||||||
<>
|
<>
|
||||||
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
|
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
|
||||||
Validated ledger index:{" "}
|
Transaction hash:{" "}
|
||||||
<Link
|
<Link
|
||||||
as="a"
|
as="a"
|
||||||
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.validated_ledger_index}`}
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{submitRes.validated_ledger_index}
|
{submitRes.tx_json?.hash}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -168,7 +189,7 @@ export const deployHook = async (
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
state.deployLogs.push({
|
state.deployLogs.push({
|
||||||
type: "error",
|
type: "error",
|
||||||
message: "Error occured while deploying",
|
message: "Error occurred while deploying",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (currentAccount) {
|
if (currentAccount) {
|
||||||
@@ -183,7 +204,7 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentAccount = state.accounts.find(
|
const currentAccount = state.accounts.find(
|
||||||
acc => acc.address === account.address
|
(acc) => acc.address === account.address
|
||||||
);
|
);
|
||||||
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
|
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
|
||||||
return;
|
return;
|
||||||
@@ -205,6 +226,14 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const keypair = derive.familySeed(account.secret);
|
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);
|
const { signedTransaction } = sign(tx, keypair);
|
||||||
|
|
||||||
if (currentAccount) {
|
if (currentAccount) {
|
||||||
@@ -243,10 +272,10 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
toast.error("Error occured while deleting hoook", { id: toastId });
|
toast.error("Error occurred while deleting hook", { id: toastId });
|
||||||
state.deployLogs.push({
|
state.deployLogs.push({
|
||||||
type: "error",
|
type: "error",
|
||||||
message: "Error occured while deleting hook",
|
message: "Error occurred while deleting hook",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (currentAccount) {
|
if (currentAccount) {
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ export const downloadAsZip = async () => {
|
|||||||
state.zipLoading = true
|
state.zipLoading = true
|
||||||
// TODO do something about file/gist loading state
|
// TODO do something about file/gist loading state
|
||||||
const files = state.files.map(({ name, content }) => ({ name, content }));
|
const files = state.files.map(({ name, content }) => ({ name, content }));
|
||||||
const zipped = await createZip(files);
|
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);
|
const zipFileName = guessZipFileName(files);
|
||||||
zipped.saveFile(zipFileName);
|
zipped.saveFile(zipFileName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Error occured while creating zip file, try again later')
|
toast.error('Error occurred while creating zip file, try again later')
|
||||||
} finally {
|
} finally {
|
||||||
state.zipLoading = false
|
state.zipLoading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,22 @@ export const fetchFiles = (gistId: string) => {
|
|||||||
octokit
|
octokit
|
||||||
.request("GET /gists/{gist_id}", { gist_id: gistId })
|
.request("GET /gists/{gist_id}", { gist_id: gistId })
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
if (!Object.values(templateFileIds).includes(gistId)) {
|
if (!Object.values(templateFileIds).map(v => v.id).includes(gistId)) {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
// in case of templates, fetch header file(s) and append to res
|
// in case of templates, fetch header file(s) and append to res
|
||||||
let resHeaderJson;
|
|
||||||
try {
|
try {
|
||||||
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
|
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
|
||||||
if (resHeader.ok) {
|
if (resHeader.ok) {
|
||||||
resHeaderJson = await resHeader.json();
|
const resHeaderJson = await resHeader.json()
|
||||||
|
const headerFiles: Record<string, { filename: string; content: string; language: string }> = {};
|
||||||
|
Object.entries(resHeaderJson).forEach(([key, value]) => {
|
||||||
|
const fname = `${key}.h`;
|
||||||
|
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
|
||||||
|
})
|
||||||
const files = {
|
const files = {
|
||||||
...res.data.files,
|
...res.data.files,
|
||||||
'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: resHeaderJson.hookapi, language: 'C' },
|
...headerFiles
|
||||||
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: resHeaderJson.hookmacro, language: 'C' },
|
|
||||||
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: resHeaderJson.sfcodes, language: 'C' },
|
|
||||||
};
|
};
|
||||||
res.data.files = files;
|
res.data.files = files;
|
||||||
}
|
}
|
||||||
@@ -58,6 +60,29 @@ export const fetchFiles = (gistId: string) => {
|
|||||||
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
|
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
|
||||||
content: res.data.files?.[filename]?.content || "",
|
content: res.data.files?.[filename]?.content || "",
|
||||||
}));
|
}));
|
||||||
|
// Sort files so that the source files are first
|
||||||
|
// In case of other files leave the order as it its
|
||||||
|
files.sort((a, b) => {
|
||||||
|
const aBasename = a.name.split('.')?.[0];
|
||||||
|
const aCext = a.name?.toLowerCase().endsWith('.c');
|
||||||
|
const bBasename = b.name.split('.')?.[0];
|
||||||
|
const bCext = b.name?.toLowerCase().endsWith('.c');
|
||||||
|
// If a has c extension and b doesn't move a up
|
||||||
|
if (aCext && !bCext) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!aCext && bCext) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// Otherwise fallback to default sorting based on basename
|
||||||
|
if (aBasename > bBasename) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (bBasename > aBasename) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
state.logs.push({
|
state.logs.push({
|
||||||
@@ -89,4 +114,4 @@ export const fetchFiles = (gistId: string) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import state from '../index';
|
|||||||
import { names } from './addFaucetAccount';
|
import { names } from './addFaucetAccount';
|
||||||
|
|
||||||
// Adds test account to global state with secret key
|
// Adds test account to global state with secret key
|
||||||
export const importAccount = (secret: string) => {
|
export const importAccount = (secret: string, name?: string) => {
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
return toast.error("You need to add secret!");
|
return toast.error("You need to add secret!");
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export const importAccount = (secret: string) => {
|
|||||||
if (err?.message) {
|
if (err?.message) {
|
||||||
toast.error(err.message)
|
toast.error(err.message)
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error occured while importing account')
|
toast.error('Error occurred while importing account')
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ export const importAccount = (secret: string) => {
|
|||||||
return toast.error(`Couldn't create account!`);
|
return toast.error(`Couldn't create account!`);
|
||||||
}
|
}
|
||||||
state.accounts.push({
|
state.accounts.push({
|
||||||
name: names[state.accounts.length],
|
name: name || names[state.accounts.length],
|
||||||
address: account.address || "",
|
address: account.address || "",
|
||||||
secret: account.secret.familySeed || "",
|
secret: account.secret.familySeed || "",
|
||||||
xrp: "0",
|
xrp: "0",
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import toast from "react-hot-toast";
|
|||||||
import state from '../index';
|
import state from '../index';
|
||||||
|
|
||||||
// Saves the current editor content to global state
|
// Saves the current editor content to global state
|
||||||
export const saveFile = (showToast: boolean = true) => {
|
export const saveFile = (showToast: boolean = true, activeId?: number) => {
|
||||||
const editorModels = state.editorCtx?.getModels();
|
const editorModels = state.editorCtx?.getModels();
|
||||||
const sought = '/' + state.files[state.active].name;
|
const sought = '/' + state.files[state.active].name;
|
||||||
const currentModel = editorModels?.find((editorModel) => {
|
const currentModel = editorModels?.find((editorModel) => {
|
||||||
return editorModel.uri.path.endsWith(sought);
|
return editorModel.uri.path.endsWith(sought);
|
||||||
});
|
});
|
||||||
|
const file = state.files[activeId || state.active]
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
state.files[state.active].content = currentModel?.getValue() || "";
|
file.content = currentModel?.getValue() || "";
|
||||||
}
|
}
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
toast.success("Saved successfully", { position: "bottom-center" });
|
toast.success("Saved successfully", { position: "bottom-center" });
|
||||||
|
|||||||
@@ -20,14 +20,10 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
|
|||||||
const { Fee = "1000", ...opts } = txOptions
|
const { Fee = "1000", ...opts } = txOptions
|
||||||
const tx: TransactionOptions = {
|
const tx: TransactionOptions = {
|
||||||
Account: account.address,
|
Account: account.address,
|
||||||
Sequence: account.sequence, // TODO auto-fillable
|
Sequence: account.sequence,
|
||||||
Fee, // TODO auto-fillable
|
Fee, // TODO auto-fillable default
|
||||||
...opts
|
...opts
|
||||||
};
|
};
|
||||||
const currAcc = state.accounts.find(acc => acc.address === account.address);
|
|
||||||
if (currAcc) {
|
|
||||||
currAcc.sequence = account.sequence + 1;
|
|
||||||
}
|
|
||||||
const { logPrefix = '' } = options || {}
|
const { logPrefix = '' } = options || {}
|
||||||
try {
|
try {
|
||||||
const signedAccount = derive.familySeed(account.secret);
|
const signedAccount = derive.familySeed(account.secret);
|
||||||
@@ -47,6 +43,10 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
|
|||||||
message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
|
message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const currAcc = state.accounts.find(acc => acc.address === account.address);
|
||||||
|
if (currAcc && response.account_sequence_next) {
|
||||||
|
currAcc.sequence = response.account_sequence_next;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
state.transactionLogs.push({
|
state.transactionLogs.push({
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
// export const templateFileIds = {
|
import Carbon from "../../components/icons/Carbon";
|
||||||
// 'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept
|
import Firewall from "../../components/icons/Firewall";
|
||||||
// 'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26',
|
import Notary from "../../components/icons/Notary";
|
||||||
// 'notary': 'a789c75f591eeab7932fd702ed8cf9ea',
|
import Peggy from "../../components/icons/Peggy";
|
||||||
// 'carbon': '43925143fa19735d8c6505c34d3a6a47',
|
import Starter from "../../components/icons/Starter";
|
||||||
// 'peggy': 'ceaf352e2a65741341033ab7ef05c448',
|
|
||||||
// 'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const templateFileIds = {
|
export const templateFileIds = {
|
||||||
'starter': '1f7d2963d9e342ea092286115274f3e3',
|
'starter': {
|
||||||
'firewall': '70edec690f0de4dd315fad1f4f996d8c',
|
id: '9106f1fe60482d90475bfe8f1315affe',
|
||||||
'notary': '3d5677768fe8a54c4f6317e185d9ba66',
|
name: 'Starter',
|
||||||
'carbon': 'a9fbcaf1b816b198c7fc0f62962bebf2',
|
description: 'Just a basic starter with essential imports, just accepts any transaction coming through',
|
||||||
'doubler': '56b86174aeb70b2b48eee962bad3e355',
|
icon: Starter
|
||||||
'peggy': 'd21298a37e1550b781682014762a567b',
|
|
||||||
'headers': '55f639bce59a49c58c45e663776b5138'
|
},
|
||||||
|
'firewall': {
|
||||||
|
id: '1cc30f39c8a0b9c55b88c312669ca45e', // Forked
|
||||||
|
name: 'Firewall',
|
||||||
|
description: 'This Hook essentially checks a blacklist of accounts',
|
||||||
|
icon: Firewall
|
||||||
|
},
|
||||||
|
'notary': {
|
||||||
|
id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked
|
||||||
|
name: 'Notary',
|
||||||
|
description: 'Collecting signatures for multi-sign transactions',
|
||||||
|
icon: Notary
|
||||||
|
},
|
||||||
|
'carbon': {
|
||||||
|
id: '5941c19dce3e147948f564e224553c02',
|
||||||
|
name: 'Carbon',
|
||||||
|
description: 'Send a percentage of sum to an address',
|
||||||
|
icon: Carbon
|
||||||
|
},
|
||||||
|
'peggy': {
|
||||||
|
id: '049784a83fa068faf7912f663f7b6471', // Forked
|
||||||
|
name: 'Peggy',
|
||||||
|
description: 'An oracle based stable coin hook',
|
||||||
|
icon: Peggy
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'hookmacro.h']
|
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h'];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type monaco from "monaco-editor";
|
import type monaco from "monaco-editor";
|
||||||
import { proxy, ref, subscribe } from "valtio";
|
import { proxy, ref, subscribe } from "valtio";
|
||||||
import { devtools } from 'valtio/utils';
|
import { devtools, subscribeKey } from 'valtio/utils';
|
||||||
import { XrplClient } from "xrpl-client";
|
import { XrplClient } from "xrpl-client";
|
||||||
import { SplitSize } from "./actions/persistSplits";
|
import { SplitSize } from "./actions/persistSplits";
|
||||||
|
|
||||||
@@ -13,9 +13,11 @@ export interface IFile {
|
|||||||
name: string;
|
name: string;
|
||||||
language: string;
|
language: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
compiledValueSnapshot?: string
|
||||||
compiledContent?: ArrayBuffer | null;
|
compiledContent?: ArrayBuffer | null;
|
||||||
compiledWatContent?: string | null;
|
compiledWatContent?: string | null;
|
||||||
lastCompiled?: Date
|
lastCompiled?: Date
|
||||||
|
containsErrors?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FaucetAccountRes {
|
export interface FaucetAccountRes {
|
||||||
@@ -35,6 +37,10 @@ export interface IAccount {
|
|||||||
hooks: string[];
|
hooks: string[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
error?: {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILog {
|
export interface ILog {
|
||||||
@@ -48,6 +54,8 @@ export interface ILog {
|
|||||||
defaultCollapsed?: boolean
|
defaultCollapsed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeployValue = Record<IFile['name'], any>;
|
||||||
|
|
||||||
export interface IState {
|
export interface IState {
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
gistId?: string | null;
|
gistId?: string | null;
|
||||||
@@ -62,6 +70,7 @@ export interface IState {
|
|||||||
logs: ILog[];
|
logs: ILog[];
|
||||||
deployLogs: ILog[];
|
deployLogs: ILog[];
|
||||||
transactionLogs: ILog[];
|
transactionLogs: ILog[];
|
||||||
|
scriptLogs: ILog[];
|
||||||
editorCtx?: typeof monaco.editor;
|
editorCtx?: typeof monaco.editor;
|
||||||
editorSettings: {
|
editorSettings: {
|
||||||
tabSize: number;
|
tabSize: number;
|
||||||
@@ -77,7 +86,8 @@ export interface IState {
|
|||||||
compileOptions: {
|
compileOptions: {
|
||||||
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
|
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
|
||||||
strip: boolean
|
strip: boolean
|
||||||
}
|
},
|
||||||
|
deployValues: DeployValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// let localStorageState: null | string = null;
|
// let localStorageState: null | string = null;
|
||||||
@@ -92,6 +102,7 @@ let initialState: IState = {
|
|||||||
logs: [],
|
logs: [],
|
||||||
deployLogs: [],
|
deployLogs: [],
|
||||||
transactionLogs: [],
|
transactionLogs: [],
|
||||||
|
scriptLogs: [],
|
||||||
editorCtx: undefined,
|
editorCtx: undefined,
|
||||||
gistId: undefined,
|
gistId: undefined,
|
||||||
gistOwner: undefined,
|
gistOwner: undefined,
|
||||||
@@ -108,9 +119,10 @@ let initialState: IState = {
|
|||||||
mainModalShowed: false,
|
mainModalShowed: false,
|
||||||
accounts: [],
|
accounts: [],
|
||||||
compileOptions: {
|
compileOptions: {
|
||||||
optimizationLevel: '-O0',
|
optimizationLevel: '-O2',
|
||||||
strip: false
|
strip: true
|
||||||
}
|
},
|
||||||
|
deployValues: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
let localStorageAccounts: string | null = null;
|
let localStorageAccounts: string | null = null;
|
||||||
@@ -156,16 +168,23 @@ if (process.env.NODE_ENV !== "production") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
subscribe(state, () => {
|
subscribe(state.accounts, () => {
|
||||||
const { accounts, active } = state;
|
const { accounts } = state;
|
||||||
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
|
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
|
||||||
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
|
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
|
||||||
if (!state.files[active]?.compiledWatContent) {
|
|
||||||
state.activeWat = 0;
|
|
||||||
} else {
|
|
||||||
state.activeWat = active;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateActiveWat = () => {
|
||||||
|
const filename = state.files[state.active]?.name
|
||||||
|
|
||||||
|
const compiledFiles = state.files.filter(
|
||||||
|
file => file.compiledContent)
|
||||||
|
const idx = compiledFiles.findIndex(file => file.name === filename)
|
||||||
|
|
||||||
|
if (idx !== -1) state.activeWat = idx
|
||||||
|
}
|
||||||
|
subscribeKey(state, 'active', updateActiveWat)
|
||||||
|
subscribeKey(state, 'files', updateActiveWat)
|
||||||
}
|
}
|
||||||
export default state
|
export default state
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ export interface TransactionState {
|
|||||||
txIsDisabled: boolean;
|
txIsDisabled: boolean;
|
||||||
txFields: TxFields;
|
txFields: TxFields;
|
||||||
viewType: 'json' | 'ui',
|
viewType: 'json' | 'ui',
|
||||||
editorSavedValue: null | string,
|
editorValue?: string,
|
||||||
editorValue?: string
|
estimatedFee?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type TxFields = Omit<
|
export type TxFields = Omit<
|
||||||
typeof transactionsData[0],
|
Partial<typeof transactionsData[0]>,
|
||||||
"Account" | "Sequence" | "TransactionType"
|
"Account" | "Sequence" | "TransactionType"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -35,27 +35,34 @@ export const defaultTransaction: TransactionState = {
|
|||||||
txIsLoading: false,
|
txIsLoading: false,
|
||||||
txIsDisabled: false,
|
txIsDisabled: false,
|
||||||
txFields: {},
|
txFields: {},
|
||||||
viewType: 'ui',
|
viewType: 'ui'
|
||||||
editorSavedValue: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transactionsState = proxy({
|
export const transactionsState = proxy({
|
||||||
transactions: [
|
transactions: [
|
||||||
{
|
{
|
||||||
header: "test1.json",
|
header: "test1.json",
|
||||||
state: defaultTransaction,
|
state: { ...defaultTransaction },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
activeHeader: "test1.json"
|
activeHeader: "test1.json"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const renameTxState = (oldName: string, nwName: string) => {
|
||||||
|
const tx = transactionsState.transactions.find(tx => tx.header === oldName);
|
||||||
|
|
||||||
|
if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`);
|
||||||
|
|
||||||
|
tx.header = nwName
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple transaction state changer
|
* Simple transaction state changer
|
||||||
* @param header Unique key and tab name for the transaction tab
|
* @param header Unique key and tab name for the transaction tab
|
||||||
* @param partialTx partial transaction state, `undefined` deletes the transaction
|
* @param partialTx partial transaction state, `undefined` deletes the transaction
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const modifyTransaction = (
|
export const modifyTxState = (
|
||||||
header: string,
|
header: string,
|
||||||
partialTx?: Partial<TransactionState>,
|
partialTx?: Partial<TransactionState>,
|
||||||
opts: { replaceState?: boolean } = {}
|
opts: { replaceState?: boolean } = {}
|
||||||
@@ -91,9 +98,9 @@ export const modifyTransaction = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(partialTx).forEach(k => {
|
Object.keys(partialTx).forEach(k => {
|
||||||
// Typescript mess here, but is definetly safe!
|
// Typescript mess here, but is definitely safe!
|
||||||
const s = tx.state as any;
|
const s = tx.state as any;
|
||||||
const p = partialTx as any;
|
const p = partialTx as any; // ? Make copy
|
||||||
if (!deepEqual(s[k], p[k])) s[k] = p[k];
|
if (!deepEqual(s[k], p[k])) s[k] = p[k];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,7 +124,7 @@ export const prepareTransaction = (data: any) => {
|
|||||||
// handle type: `json`
|
// handle type: `json`
|
||||||
if (_value && typeof _value === "object" && _value.$type === "json") {
|
if (_value && typeof _value === "object" && _value.$type === "json") {
|
||||||
if (typeof _value.$value === "object") {
|
if (typeof _value.$value === "object") {
|
||||||
options[field] = _value.$value as any;
|
options[field] = _value.$value;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
options[field] = JSON.parse(_value.$value);
|
options[field] = JSON.parse(_value.$value);
|
||||||
@@ -130,8 +137,8 @@ export const prepareTransaction = (data: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete unneccesary fields
|
// delete unnecessary fields
|
||||||
if (options[field] === undefined) {
|
if (!options[field]) {
|
||||||
delete options[field];
|
delete options[field];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -140,7 +147,7 @@ export const prepareTransaction = (data: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// editor value to state
|
// editor value to state
|
||||||
export const prepareState = (value: string, txState: TransactionState) => {
|
export const prepareState = (value: string, transactionType?: string) => {
|
||||||
const options = parseJSON(value);
|
const options = parseJSON(value);
|
||||||
if (!options) {
|
if (!options) {
|
||||||
showAlert("Error!", {
|
showAlert("Error!", {
|
||||||
@@ -151,7 +158,7 @@ export const prepareState = (value: string, txState: TransactionState) => {
|
|||||||
|
|
||||||
const { Account, TransactionType, Destination, ...rest } = options;
|
const { Account, TransactionType, Destination, ...rest } = options;
|
||||||
let tx: Partial<TransactionState> = {};
|
let tx: Partial<TransactionState> = {};
|
||||||
const { txFields } = txState
|
const schema = getTxFields(transactionType)
|
||||||
|
|
||||||
if (Account) {
|
if (Account) {
|
||||||
const acc = state.accounts.find(acc => acc.address === Account);
|
const acc = state.accounts.find(acc => acc.address === Account);
|
||||||
@@ -179,9 +186,8 @@ export const prepareState = (value: string, txState: TransactionState) => {
|
|||||||
tx.selectedTransaction = null;
|
tx.selectedTransaction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (txFields.Destination !== undefined) {
|
if (schema.Destination !== undefined) {
|
||||||
const dest = state.accounts.find(acc => acc.address === Destination);
|
const dest = state.accounts.find(acc => acc.address === Destination);
|
||||||
rest.Destination = null
|
|
||||||
if (dest) {
|
if (dest) {
|
||||||
tx.selectedDestAccount = {
|
tx.selectedDestAccount = {
|
||||||
label: dest.name,
|
label: dest.name,
|
||||||
@@ -198,15 +204,18 @@ export const prepareState = (value: string, txState: TransactionState) => {
|
|||||||
tx.selectedDestAccount = null
|
tx.selectedDestAccount = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (Destination) {
|
||||||
|
rest.Destination = Destination
|
||||||
|
}
|
||||||
|
|
||||||
Object.keys(rest).forEach(field => {
|
Object.keys(rest).forEach(field => {
|
||||||
const value = rest[field];
|
const value = rest[field];
|
||||||
const origValue = txFields[field as keyof TxFields]
|
const schemaVal = schema[field as keyof TxFields]
|
||||||
const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.$type === 'xrp'
|
const isXrp = typeof value !== 'object' && schemaVal && typeof schemaVal === 'object' && schemaVal.$type === 'xrp'
|
||||||
if (isXrp) {
|
if (isXrp) {
|
||||||
rest[field] = {
|
rest[field] = {
|
||||||
$type: "xrp",
|
$type: "xrp",
|
||||||
$value: +value / 1000000, // TODO maybe use bigint?
|
$value: +value / 1000000, // ! maybe use bigint?
|
||||||
};
|
};
|
||||||
} else if (typeof value === "object") {
|
} else if (typeof value === "object") {
|
||||||
rest[field] = {
|
rest[field] = {
|
||||||
@@ -217,9 +226,35 @@ export const prepareState = (value: string, txState: TransactionState) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tx.txFields = rest;
|
tx.txFields = rest;
|
||||||
tx.editorSavedValue = null;
|
|
||||||
|
|
||||||
return tx
|
return tx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTxFields = (tt?: string) => {
|
||||||
|
const txFields: TxFields | undefined = transactionsData.find(
|
||||||
|
tx => tx.TransactionType === tt
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!txFields) return {}
|
||||||
|
|
||||||
|
let _txFields = Object.keys(txFields)
|
||||||
|
.filter(
|
||||||
|
key => !["TransactionType", "Account", "Sequence"].includes(key)
|
||||||
|
)
|
||||||
|
.reduce<TxFields>(
|
||||||
|
(tf, key) => (
|
||||||
|
(tf[key as keyof TxFields] = (txFields as any)[key]), tf
|
||||||
|
),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return _txFields
|
||||||
|
}
|
||||||
|
|
||||||
export { transactionsData }
|
export { transactionsData }
|
||||||
|
|
||||||
|
export const transactionsOptions = transactionsData.map(tx => ({
|
||||||
|
value: tx.TransactionType,
|
||||||
|
label: tx.TransactionType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment')
|
||||||
@@ -53,6 +53,7 @@ export const {
|
|||||||
accent: "#9D2DFF",
|
accent: "#9D2DFF",
|
||||||
background: "$gray1",
|
background: "$gray1",
|
||||||
backgroundAlt: "$gray4",
|
backgroundAlt: "$gray4",
|
||||||
|
backgroundOverlay: "$mauve2",
|
||||||
text: "$gray12",
|
text: "$gray12",
|
||||||
textMuted: "$gray10",
|
textMuted: "$gray10",
|
||||||
primary: "$plum",
|
primary: "$plum",
|
||||||
@@ -365,6 +366,7 @@ export const darkTheme = createTheme("dark", {
|
|||||||
...greenDark,
|
...greenDark,
|
||||||
...redDark,
|
...redDark,
|
||||||
deep: "rgb(10, 10, 10)",
|
deep: "rgb(10, 10, 10)",
|
||||||
|
backgroundOverlay: "$mauve5"
|
||||||
// backgroundA: transparentize(0.1, grayDark.gray1),
|
// backgroundA: transparentize(0.1, grayDark.gray1),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
21
styles/keyframes.ts
Normal file
21
styles/keyframes.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { keyframes } from '../stitches.config';
|
||||||
|
|
||||||
|
export const slideUpAndFade = keyframes({
|
||||||
|
"0%": { opacity: 0, transform: "translateY(2px)" },
|
||||||
|
"100%": { opacity: 1, transform: "translateY(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const slideRightAndFade = keyframes({
|
||||||
|
"0%": { opacity: 0, transform: "translateX(-2px)" },
|
||||||
|
"100%": { opacity: 1, transform: "translateX(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const slideDownAndFade = keyframes({
|
||||||
|
"0%": { opacity: 0, transform: "translateY(-2px)" },
|
||||||
|
"100%": { opacity: 1, transform: "translateY(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const slideLeftAndFade = keyframes({
|
||||||
|
"0%": { opacity: 0, transform: "translateX(2px)" },
|
||||||
|
"100%": { opacity: 1, transform: "translateX(0)" },
|
||||||
|
});
|
||||||
24
utils/comment-parser.ts
Normal file
24
utils/comment-parser.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Spec, parse, Problem } from "comment-parser"
|
||||||
|
|
||||||
|
export const getTags = (source?: string): Spec[] => {
|
||||||
|
if (!source) return []
|
||||||
|
const blocks = parse(source)
|
||||||
|
const tags = blocks.reduce(
|
||||||
|
(acc, block) => acc.concat(block.tags),
|
||||||
|
[] as Spec[]
|
||||||
|
);
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getErrors = (source?: string): Error | undefined => {
|
||||||
|
if (!source) return undefined
|
||||||
|
const blocks = parse(source)
|
||||||
|
const probs = blocks.reduce(
|
||||||
|
(acc, block) => acc.concat(block.problems),
|
||||||
|
[] as Problem[]
|
||||||
|
);
|
||||||
|
if (!probs.length) return undefined
|
||||||
|
const errors = probs.map(prob => `[${prob.code}] on line ${prob.line}: ${prob.message}`)
|
||||||
|
const error = new Error(`The following error(s) occurred while parsing JSDOC: \n${errors.join('\n')}`)
|
||||||
|
return error
|
||||||
|
}
|
||||||
30
utils/estimateFee.ts
Normal file
30
utils/estimateFee.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { derive, sign } from "xrpl-accountlib"
|
||||||
|
import state, { IAccount } from "../state"
|
||||||
|
|
||||||
|
const estimateFee = async (tx: Record<string, unknown>, account: IAccount, opts: { silent?: boolean } = {}): Promise<null | { base_fee: string, median_fee: string; minimum_fee: string; open_ledger_fee: string; }> => {
|
||||||
|
try {
|
||||||
|
const copyTx = JSON.parse(JSON.stringify(tx))
|
||||||
|
delete copyTx['SigningPubKey']
|
||||||
|
if (!copyTx.Fee) {
|
||||||
|
copyTx.Fee = '1000'
|
||||||
|
}
|
||||||
|
|
||||||
|
const keypair = derive.familySeed(account.secret)
|
||||||
|
const { signedTransaction } = sign(copyTx, keypair);
|
||||||
|
|
||||||
|
const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction })
|
||||||
|
if (res && res.drops) {
|
||||||
|
return res.drops;
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (err) {
|
||||||
|
if (!opts.silent) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error("Cannot estimate fee.") // ? Some better msg
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default estimateFee
|
||||||
@@ -6,4 +6,10 @@ export const guessZipFileName = (files: File[]) => {
|
|||||||
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
|
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
|
||||||
parts = parts.length > 1 ? parts.slice(0, -1) : parts
|
parts = parts.length > 1 ? parts.slice(0, -1) : parts
|
||||||
return parts.join('')
|
return parts.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const capitalize = (value?: string) => {
|
||||||
|
if (!value) return '';
|
||||||
|
|
||||||
|
return value[0].toLocaleUpperCase() + value.slice(1);
|
||||||
}
|
}
|
||||||
@@ -18,13 +18,18 @@ export const tts = {
|
|||||||
ttDEPOSIT_PREAUTH: 19,
|
ttDEPOSIT_PREAUTH: 19,
|
||||||
ttTRUST_SET: 20,
|
ttTRUST_SET: 20,
|
||||||
ttACCOUNT_DELETE: 21,
|
ttACCOUNT_DELETE: 21,
|
||||||
ttHOOK_SET: 22
|
ttHOOK_SET: 22,
|
||||||
|
ttNFTOKEN_MINT: 25,
|
||||||
|
ttNFTOKEN_BURN: 26,
|
||||||
|
ttNFTOKEN_CREATE_OFFER: 27,
|
||||||
|
ttNFTOKEN_CANCEL_OFFER: 28,
|
||||||
|
ttNFTOKEN_ACCEPT_OFFER: 29
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TTS = typeof tts;
|
export type TTS = typeof tts;
|
||||||
|
|
||||||
const calculateHookOn = (arr: (keyof TTS)[]) => {
|
const calculateHookOn = (arr: (keyof TTS)[]) => {
|
||||||
let start = '0x00000000003ff5bf';
|
let start = '0x000000003e3ff5bf';
|
||||||
arr.forEach(n => {
|
arr.forEach(n => {
|
||||||
let v = BigInt(start);
|
let v = BigInt(start);
|
||||||
v ^= (BigInt(1) << BigInt(tts[n as keyof TTS]));
|
v ^= (BigInt(1) << BigInt(tts[n as keyof TTS]));
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { MessageConnection } from "@codingame/monaco-jsonrpc";
|
import { MessageConnection } from "@codingame/monaco-jsonrpc";
|
||||||
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
|
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
|
||||||
import Router from "next/router";
|
|
||||||
import normalizeUrl from "normalize-url";
|
import normalizeUrl from "normalize-url";
|
||||||
import ReconnectingWebSocket from "reconnecting-websocket";
|
import ReconnectingWebSocket from "reconnecting-websocket";
|
||||||
|
|
||||||
@@ -14,11 +13,7 @@ export function createLanguageClient(connection: MessageConnection): MonacoLangu
|
|||||||
errorHandler: {
|
errorHandler: {
|
||||||
error: () => ErrorAction.Continue,
|
error: () => ErrorAction.Continue,
|
||||||
closed: () => {
|
closed: () => {
|
||||||
if (Router.pathname.includes('/develop')) {
|
return CloseAction.DoNotRestart
|
||||||
return CloseAction.Restart
|
|
||||||
} else {
|
|
||||||
return CloseAction.DoNotRestart
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
78
utils/setHook.ts
Normal file
78
utils/setHook.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { getTags } from './comment-parser';
|
||||||
|
import { tts, TTS } from './hookOnCalculator';
|
||||||
|
|
||||||
|
export const transactionOptions = Object.keys(tts).map(key => ({
|
||||||
|
label: key,
|
||||||
|
value: key as keyof TTS,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type SetHookData = {
|
||||||
|
Invoke: {
|
||||||
|
value: keyof TTS;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
Fee: string;
|
||||||
|
HookNamespace: string;
|
||||||
|
HookParameters: {
|
||||||
|
HookParameter: {
|
||||||
|
HookParameterName: string;
|
||||||
|
HookParameterValue: string;
|
||||||
|
};
|
||||||
|
$metaData?: any;
|
||||||
|
}[];
|
||||||
|
// HookGrants: {
|
||||||
|
// HookGrant: {
|
||||||
|
// Authorize: string;
|
||||||
|
// HookHash: string;
|
||||||
|
// };
|
||||||
|
// }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getParameters = (content?: string) => {
|
||||||
|
const fieldTags = ["field", "param", "arg", "argument"];
|
||||||
|
const tags = getTags(content)
|
||||||
|
.filter(tag => fieldTags.includes(tag.tag))
|
||||||
|
.filter(tag => !!tag.name);
|
||||||
|
|
||||||
|
const paramters: SetHookData["HookParameters"] = tags.map(tag => ({
|
||||||
|
HookParameter: {
|
||||||
|
HookParameterName: tag.name,
|
||||||
|
HookParameterValue: tag.default || "",
|
||||||
|
},
|
||||||
|
$metaData: {
|
||||||
|
description: tag.description,
|
||||||
|
required: !tag.optional
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return paramters;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInvokeOptions = (content?: string) => {
|
||||||
|
const invokeTags = ["invoke", "invoke-on"];
|
||||||
|
|
||||||
|
const options = getTags(content)
|
||||||
|
.filter(tag => invokeTags.includes(tag.tag))
|
||||||
|
.reduce((cumm, curr) => {
|
||||||
|
const combined = curr.type || `${curr.name} ${curr.description}`
|
||||||
|
const opts = combined.split(' ')
|
||||||
|
|
||||||
|
return cumm.concat(opts as any)
|
||||||
|
}, [] as (keyof TTS)[])
|
||||||
|
.filter(opt => Object.keys(tts).includes(opt))
|
||||||
|
|
||||||
|
|
||||||
|
const invokeOptions: SetHookData['Invoke'] = options.map(opt => ({
|
||||||
|
label: opt,
|
||||||
|
value: opt
|
||||||
|
}))
|
||||||
|
|
||||||
|
// default
|
||||||
|
if (!invokeOptions.length) {
|
||||||
|
const payment = transactionOptions.find(tx => tx.value === "ttPAYMENT")
|
||||||
|
if (payment) return [payment]
|
||||||
|
}
|
||||||
|
|
||||||
|
return invokeOptions;
|
||||||
|
};
|
||||||
@@ -41,6 +41,7 @@ import hooksSkipHashBufLen from "./md/hooks-skip-hash-buf-len.md";
|
|||||||
import hooksStateBufLen from "./md/hooks-state-buf-len.md";
|
import hooksStateBufLen from "./md/hooks-state-buf-len.md";
|
||||||
import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md";
|
import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md";
|
||||||
import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.md";
|
import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.md";
|
||||||
|
import hooksTrivialCbak from "./md/hooks-trivial-cbak.md";
|
||||||
import hooksValidateBufLen from "./md/hooks-validate-buf-len.md";
|
import hooksValidateBufLen from "./md/hooks-validate-buf-len.md";
|
||||||
import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md";
|
import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md";
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ const docs: { [key: string]: string; } = {
|
|||||||
"hooks-state-buf-len": hooksStateBufLen,
|
"hooks-state-buf-len": hooksStateBufLen,
|
||||||
"hooks-transaction-hash-buf-len": hooksTransactionHashBufLen,
|
"hooks-transaction-hash-buf-len": hooksTransactionHashBufLen,
|
||||||
"hooks-transaction-slot-limit": hooksTransactionSlotLimit,
|
"hooks-transaction-slot-limit": hooksTransactionSlotLimit,
|
||||||
|
"hooks-trivial-cbak": hooksTrivialCbak,
|
||||||
"hooks-validate-buf-len": hooksValidateBufLen,
|
"hooks-validate-buf-len": hooksValidateBufLen,
|
||||||
"hooks-verify-buf-len": hooksVerifyBufLen,
|
"hooks-verify-buf-len": hooksVerifyBufLen,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# hooks-entry-points
|
# hooks-entry-points
|
||||||
|
|
||||||
A Hook always implements and exports exactly two functions: [cbak](https://xrpl-hooks.readme.io/v2.0/reference/cbak) and [hook](https://xrpl-hooks.readme.io/v2.0/reference/hook).
|
A Hook always implements and exports a [hook](https://xrpl-hooks.readme.io/v2.0/reference/hook) function.
|
||||||
|
|
||||||
This check shows error on translation units that do not have them.
|
This check shows error on translation units that do not have it.
|
||||||
|
|
||||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)
|
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# hooks-hash-buf-len
|
# hooks-hash-buf-len
|
||||||
|
|
||||||
Functions [util_sha512h](https://xrpl-hooks.readme.io/v2.0/reference/util_sha512h), [hook_hash](https://xrpl-hooks.readme.io/v2.0/reference/hook_hash), [ledger_last_hash](https://xrpl-hooks.readme.io/v2.0/reference/ledger_last_hash) and [nonce](https://xrpl-hooks.readme.io/v2.0/reference/nonce) have fixed-size hash output.
|
Functions [util_sha512h](https://xrpl-hooks.readme.io/v2.0/reference/util_sha512h), [hook_hash](https://xrpl-hooks.readme.io/v2.0/reference/hook_hash), [ledger_last_hash](https://xrpl-hooks.readme.io/v2.0/reference/ledger_last_hash), [etxn_nonce](https://xrpl-hooks.readme.io/v2.0/reference/etxn_nonce) and [ledger_nonce](https://xrpl-hooks.readme.io/v2.0/reference/ledger_nonce) have fixed-size hash output.
|
||||||
|
|
||||||
This check warns about too-small size of their output buffer (if it's specified by a constant - variable parameter is ignored).
|
This check warns about too-small size of their output buffer (if it's specified by a constant - variable parameter is ignored).
|
||||||
|
|||||||
7
xrpl-hooks-docs/md/hooks-trivial-cbak.md
Normal file
7
xrpl-hooks-docs/md/hooks-trivial-cbak.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# hooks-trivial-cbak
|
||||||
|
|
||||||
|
A Hook may implement and export a [cbak](https://xrpl-hooks.readme.io/v2.0/reference/cbak) function.
|
||||||
|
|
||||||
|
But the function is optional, and defining it so that it doesn't do anything besides returning a constant value is unnecessary (except for some debugging scenarios) and just increases the hook size. This check warns about such implementations.
|
||||||
|
|
||||||
|
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)
|
||||||
127
yarn.lock
127
yarn.lock
@@ -202,19 +202,19 @@
|
|||||||
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz"
|
||||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||||
|
|
||||||
"@monaco-editor/loader@^1.3.0":
|
"@monaco-editor/loader@^1.3.2":
|
||||||
version "1.3.0"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.0.tgz#659fbaf1d612ea67b2a0519a18612d1c4813e444"
|
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.2.tgz#04effbb87052d19cd7d3c9d81c0635490f9bb6d8"
|
||||||
integrity sha512-N3mGq1ktC3zh7WUx3NGO+PSDdNq9Vspk/41rEmRdrCqV9vNbBTRzAOplmUpNQsi+hmTs++ERMBobMERb8Kb+3g==
|
integrity sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g==
|
||||||
dependencies:
|
dependencies:
|
||||||
state-local "^1.0.6"
|
state-local "^1.0.6"
|
||||||
|
|
||||||
"@monaco-editor/react@^4.4.1":
|
"@monaco-editor/react@^4.4.5":
|
||||||
version "4.4.1"
|
version "4.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.1.tgz#2e2b9b369f3082b0e14f47cdbe35658fd56c7c7d"
|
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.5.tgz#beabe491efeb2457441a00d1c7651c653697f65b"
|
||||||
integrity sha512-95E/XPC4dbm/7qdkhSsU/a1kRgcn2PYhRTVIc+/cixWCZrwRURW1DRPaIZ2lOawBJ6kAOLywxuD4A4UmbT0ZIw==
|
integrity sha512-IImtzU7sRc66OOaQVCG+5PFHkSWnnhrUWGBuH6zNmH2h0YgmAhcjHZQc/6MY9JWEbUtVF1WPBMJ9u1XuFbRrVA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@monaco-editor/loader" "^1.3.0"
|
"@monaco-editor/loader" "^1.3.2"
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
"@next/env@12.1.0":
|
"@next/env@12.1.0":
|
||||||
@@ -594,6 +594,18 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-context-menu@^0.1.6":
|
||||||
|
version "0.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-0.1.6.tgz#0c75f2faffec6c8697247a4b685a432b3c4d07f0"
|
||||||
|
integrity sha512-0qa6ABaeqD+WYI+8iT0jH0QLLcV8Kv0xI+mZL4FFnG4ec9H0v+yngb5cfBBfs9e/KM8mDzFFpaeegqsQlLNqyQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "0.1.0"
|
||||||
|
"@radix-ui/react-context" "0.1.1"
|
||||||
|
"@radix-ui/react-menu" "0.1.6"
|
||||||
|
"@radix-ui/react-primitive" "0.1.4"
|
||||||
|
"@radix-ui/react-use-callback-ref" "0.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-context@0.1.1":
|
"@radix-ui/react-context@0.1.1":
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz"
|
resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz"
|
||||||
@@ -635,9 +647,9 @@
|
|||||||
"@radix-ui/react-use-callback-ref" "0.1.0"
|
"@radix-ui/react-use-callback-ref" "0.1.0"
|
||||||
"@radix-ui/react-use-escape-keydown" "0.1.0"
|
"@radix-ui/react-use-escape-keydown" "0.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-dropdown-menu@^0.1.1":
|
"@radix-ui/react-dropdown-menu@^0.1.6":
|
||||||
version "0.1.6"
|
version "0.1.6"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz#3203229788cd57e552c9f19dcc7008e2b545919c"
|
||||||
integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==
|
integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
@@ -917,10 +929,10 @@
|
|||||||
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz"
|
||||||
integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==
|
integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==
|
||||||
|
|
||||||
"@stitches/react@^1.2.6-0":
|
"@stitches/react@^1.2.8":
|
||||||
version "1.2.7"
|
version "1.2.8"
|
||||||
resolved "https://registry.npmjs.org/@stitches/react/-/react-1.2.7.tgz"
|
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38"
|
||||||
integrity sha512-6AxpUag7OW55ANzRnuy7R15FEyQeZ66fytVo3BBilFIU0mfo3t49CAMcEAL/A1SbhSj/FCdWkn/XrbjGBTJTzg==
|
integrity sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==
|
||||||
|
|
||||||
"@types/aws-lambda@^8.10.83":
|
"@types/aws-lambda@^8.10.83":
|
||||||
version "8.10.93"
|
version "8.10.93"
|
||||||
@@ -1280,14 +1292,6 @@ babel-plugin-macros@^2.6.1:
|
|||||||
cosmiconfig "^6.0.0"
|
cosmiconfig "^6.0.0"
|
||||||
resolve "^1.12.0"
|
resolve "^1.12.0"
|
||||||
|
|
||||||
babel-runtime@^6.26.0:
|
|
||||||
version "6.26.0"
|
|
||||||
resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz"
|
|
||||||
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
|
|
||||||
dependencies:
|
|
||||||
core-js "^2.4.0"
|
|
||||||
regenerator-runtime "^0.11.0"
|
|
||||||
|
|
||||||
balanced-match@^1.0.0:
|
balanced-match@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||||
@@ -1525,6 +1529,11 @@ color-name@~1.1.4:
|
|||||||
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
|
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
|
comment-parser@^1.3.1:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b"
|
||||||
|
integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
||||||
@@ -1547,11 +1556,6 @@ core-js-pure@^3.20.2:
|
|||||||
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz"
|
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz"
|
||||||
integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ==
|
integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ==
|
||||||
|
|
||||||
core-js@^2.4.0:
|
|
||||||
version "2.6.12"
|
|
||||||
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz"
|
|
||||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
|
||||||
|
|
||||||
core-util-is@~1.0.0:
|
core-util-is@~1.0.0:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
|
||||||
@@ -2949,10 +2953,10 @@ natural-compare@^1.4.0:
|
|||||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||||
|
|
||||||
next-auth@^4.0.0-beta.5:
|
next-auth@^4.10.1:
|
||||||
version "4.2.1"
|
version "4.10.1"
|
||||||
resolved "https://registry.npmjs.org/next-auth/-/next-auth-4.2.1.tgz"
|
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.1.tgz#33b29265d12287bb2f6d267c8d415a407c27f0e9"
|
||||||
integrity sha512-XDtt7nqevkNf4EJ2zKAKkI+MFsURf11kx11vPwxrBYA1MHeqWwaWbGOUOI2ekNTvfAg4nTEJJUH3LV2cLrH3Tg==
|
integrity sha512-F00vtwBdyMIIJ8IORHOAOHjVGTDEhhm9+HpB2BQ8r40WtGxqToWWLN7Z+2ZW/z2RFlo3zhcuAtUCPUzVJxtZwQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.16.3"
|
"@babel/runtime" "^7.16.3"
|
||||||
"@panva/hkdf" "^1.0.1"
|
"@panva/hkdf" "^1.0.1"
|
||||||
@@ -2964,6 +2968,11 @@ next-auth@^4.0.0-beta.5:
|
|||||||
preact-render-to-string "^5.1.19"
|
preact-render-to-string "^5.1.19"
|
||||||
uuid "^8.3.2"
|
uuid "^8.3.2"
|
||||||
|
|
||||||
|
next-plausible@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/next-plausible/-/next-plausible-3.2.0.tgz#d801346253e0c1cf64a02b9fc3a42050455cbc47"
|
||||||
|
integrity sha512-OlYcLXBG3kKd/fKMpm8SZ5IkUKSFm1/8t7cv6e5bewIqlpdZpdWuSrjbdJpbmutb2KPLXHzilKp09zmDGjy9KQ==
|
||||||
|
|
||||||
next-themes@^0.1.1:
|
next-themes@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.1.1.tgz#122113a458bf1d1be5ffed66778ab924c106f82a"
|
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.1.1.tgz#122113a458bf1d1be5ffed66778ab924c106f82a"
|
||||||
@@ -3535,11 +3544,6 @@ reconnecting-websocket@^4.4.0:
|
|||||||
resolved "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz"
|
resolved "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz"
|
||||||
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
|
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
|
||||||
|
|
||||||
regenerator-runtime@^0.11.0:
|
|
||||||
version "0.11.1"
|
|
||||||
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz"
|
|
||||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
|
||||||
|
|
||||||
regenerator-runtime@^0.13.4:
|
regenerator-runtime@^0.13.4:
|
||||||
version "0.13.9"
|
version "0.13.9"
|
||||||
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
|
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
|
||||||
@@ -3638,30 +3642,25 @@ ripple-address-codec@^4.1.0, ripple-address-codec@^4.1.1, ripple-address-codec@^
|
|||||||
base-x "3.0.9"
|
base-x "3.0.9"
|
||||||
create-hash "^1.1.2"
|
create-hash "^1.1.2"
|
||||||
|
|
||||||
ripple-binary-codec@^0.2.4:
|
ripple-address-codec@^4.2.4:
|
||||||
version "0.2.7"
|
version "4.2.4"
|
||||||
resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-0.2.7.tgz"
|
resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-4.2.4.tgz#a56c2168c8bb81269ea4d15ed96d6824c5a866f8"
|
||||||
integrity sha512-VD+sHgZK76q3kmO765klFHPDCEveS5SUeg/bUNVpNrj7w2alyDNkbF17XNbAjFv+kSYhfsUudQanoaSs2Y6uzw==
|
integrity sha512-roAOjKz94+FboTItey1XRh5qynwt4xvfBLvbbcx+FiR94Yw2x3LrKLF2GVCMCSAh5I6PkcpADg6AbYsUbGN3nA==
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime "^6.26.0"
|
base-x "3.0.9"
|
||||||
bn.js "^5.1.1"
|
create-hash "^1.1.2"
|
||||||
create-hash "^1.2.0"
|
|
||||||
decimal.js "^10.2.0"
|
|
||||||
inherits "^2.0.4"
|
|
||||||
lodash "^4.17.15"
|
|
||||||
ripple-address-codec "^4.1.0"
|
|
||||||
|
|
||||||
ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.3.0:
|
ripple-binary-codec@=1.4.2, ripple-binary-codec@^0.2.4, ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.4.2:
|
||||||
version "1.3.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-1.3.2.tgz"
|
resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3"
|
||||||
integrity sha512-8VG1vfb3EM1J7ZdPXo9E57Zv2hF4cxT64gP6rGSQzODVgMjiBCWozhN3729qNTGtHItz0e82Oix8v95vWYBQ3A==
|
integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
assert "^2.0.0"
|
assert "^2.0.0"
|
||||||
big-integer "^1.6.48"
|
big-integer "^1.6.48"
|
||||||
buffer "5.6.0"
|
buffer "5.6.0"
|
||||||
create-hash "^1.2.0"
|
create-hash "^1.2.0"
|
||||||
decimal.js "^10.2.0"
|
decimal.js "^10.2.0"
|
||||||
ripple-address-codec "^4.2.3"
|
ripple-address-codec "^4.2.4"
|
||||||
|
|
||||||
ripple-bs58@^4.0.0:
|
ripple-bs58@^4.0.0:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
@@ -4318,10 +4317,10 @@ ws@^7.2.0:
|
|||||||
resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz"
|
resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz"
|
||||||
integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
|
integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
|
||||||
|
|
||||||
xrpl-accountlib@^1.3.2:
|
xrpl-accountlib@^1.5.2:
|
||||||
version "1.3.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.npmjs.org/xrpl-accountlib/-/xrpl-accountlib-1.3.2.tgz"
|
resolved "https://registry.yarnpkg.com/xrpl-accountlib/-/xrpl-accountlib-1.5.2.tgz#8f16abe449fd60ba9ed75597f6ce3f0c45dfff43"
|
||||||
integrity sha512-mXwoumGp0xUiZ7Ty/1o4FHVRK4uLnqngxdYmikZs50drMjlgCUP6GXun2Vf4Uus1fnVnxhXIw+E7peH5OjiOJA==
|
integrity sha512-lieY2/5G9DySqdtgQ0AD/aMMG5Sy/MLAmbIsmsCaF06scM5DpR8s4SsEzgHni7dOG68Wjnb2Uz6tf5aV+l4/Kg==
|
||||||
dependencies:
|
dependencies:
|
||||||
assert "^2.0.0"
|
assert "^2.0.0"
|
||||||
bip32 "^2.0.5"
|
bip32 "^2.0.5"
|
||||||
@@ -4330,13 +4329,13 @@ xrpl-accountlib@^1.3.2:
|
|||||||
elliptic "6.5.4"
|
elliptic "6.5.4"
|
||||||
hash.js "^1.1.7"
|
hash.js "^1.1.7"
|
||||||
ripple-address-codec "^4.1.0"
|
ripple-address-codec "^4.1.0"
|
||||||
ripple-binary-codec "^1.3.0"
|
ripple-binary-codec "^1.4.2"
|
||||||
ripple-hashes "^0.3.4"
|
ripple-hashes "^0.3.4"
|
||||||
ripple-keypairs "^1.0.3"
|
ripple-keypairs "^1.0.3"
|
||||||
ripple-lib "^1.6.4"
|
ripple-lib "^1.6.4"
|
||||||
ripple-secret-codec "^1.0.2"
|
ripple-secret-codec "^1.0.2"
|
||||||
xrpl-secret-numbers "^0.3.3"
|
xrpl-secret-numbers "^0.3.3"
|
||||||
xrpl-sign-keypairs "^2.0.1"
|
xrpl-sign-keypairs "^2.1.1"
|
||||||
|
|
||||||
xrpl-client@^1.9.4:
|
xrpl-client@^1.9.4:
|
||||||
version "1.9.4"
|
version "1.9.4"
|
||||||
@@ -4355,13 +4354,13 @@ xrpl-secret-numbers@^0.3.3:
|
|||||||
brorand "^1.1.0"
|
brorand "^1.1.0"
|
||||||
ripple-keypairs "^1.0.3"
|
ripple-keypairs "^1.0.3"
|
||||||
|
|
||||||
xrpl-sign-keypairs@^2.0.1:
|
xrpl-sign-keypairs@^2.1.1:
|
||||||
version "2.0.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.npmjs.org/xrpl-sign-keypairs/-/xrpl-sign-keypairs-2.0.1.tgz"
|
resolved "https://registry.yarnpkg.com/xrpl-sign-keypairs/-/xrpl-sign-keypairs-2.1.1.tgz#2f7f2855799c5d4ba091007963825eef1db21a4e"
|
||||||
integrity sha512-84QbE3trxetaw0hqDADCWMx0HH1VAWnTJp0TGoKTGRf1jzTqjI7eNNNw5lmcay2MH8bW/waNzJIF8vSAJSkVrQ==
|
integrity sha512-rKQmUCx+x7gjjJ5zv/Z7bOYR+8I36JwUCFlpuD9UzYD4w2msGQDG0rmxVENyZSfThDBVQ1kEArVn6SMDMe9LUQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
big-integer latest
|
big-integer latest
|
||||||
ripple-binary-codec "^1.3.0"
|
ripple-binary-codec "^1.4.2"
|
||||||
ripple-bs58check latest
|
ripple-bs58check latest
|
||||||
ripple-hashes latest
|
ripple-hashes latest
|
||||||
ripple-keypairs latest
|
ripple-keypairs latest
|
||||||
|
|||||||
Reference in New Issue
Block a user