Files
xrpl-hooks-ide/components/Accounts.tsx
Valtteri Karesto 37208ce97e fixes #201
2022-06-22 12:03:43 +03:00

554 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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