486 lines
15 KiB
TypeScript
486 lines
15 KiB
TypeScript
import toast from "react-hot-toast";
|
|
import { useSnapshot } from "valtio";
|
|
import { ArrowSquareOut, Copy, 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 } 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";
|
|
|
|
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}
|
|
</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",
|
|
}}
|
|
>
|
|
{Dinero({
|
|
amount: Number(activeAccount?.xrp || "0"),
|
|
precision: 6,
|
|
})
|
|
.toUnit()
|
|
.toLocaleString(undefined, {
|
|
style: "currency",
|
|
currency: "XRP",
|
|
currencyDisplay: "name",
|
|
})}
|
|
</Text>
|
|
</Flex>
|
|
<Flex css={{ marginLeft: "auto" }}>
|
|
<a
|
|
href={`https://hooks-testnet-explorer.xrpl-labs.com/${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 && console.log(activeAccount.hooks)}
|
|
{activeAccount &&
|
|
activeAccount.hooks.map((i) => truncate(i, 6)).join(",")}
|
|
</Text>
|
|
</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: 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;
|
|
}
|
|
});
|
|
const objectRequests = snap.accounts.map((acc) => {
|
|
return snap.client?.send({
|
|
id: `${acc.address}-hooks`,
|
|
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(), 2000);
|
|
}
|
|
|
|
return () => {
|
|
if (snap.accounts.length > 0) {
|
|
if (fetchAccountInfoInterval) {
|
|
clearInterval(fetchAccountInfoInterval);
|
|
}
|
|
}
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [snap.accounts, snap.clientStatus]);
|
|
return (
|
|
<Box
|
|
as="div"
|
|
css={{
|
|
display: "flex",
|
|
backgroundColor: props.card ? "$deep" : "$mauve1",
|
|
position: "relative",
|
|
flex: "1",
|
|
height: "100%",
|
|
border: "1px solid $mauve6",
|
|
borderRadius: props.card ? "$md" : undefined,
|
|
}}
|
|
>
|
|
<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} (
|
|
{Dinero({
|
|
amount: Number(account?.xrp || "0"),
|
|
precision: 6,
|
|
})
|
|
.toUnit()
|
|
.toLocaleString(undefined, {
|
|
style: "currency",
|
|
currency: "XRP",
|
|
currencyDisplay: "name",
|
|
})}
|
|
)
|
|
</Text>
|
|
</Box>
|
|
{!props.hideDeployBtn && (
|
|
<div
|
|
className="hook-deploy-button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
}}
|
|
>
|
|
<SetHookDialog account={account} />
|
|
</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"
|
|
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;
|