refactor: separate into components

This commit is contained in:
apoorvlathey
2023-06-10 17:00:21 +05:30
parent d2baef4a63
commit 74f85ce2f0
19 changed files with 1117 additions and 669 deletions

View File

@@ -0,0 +1,67 @@
import {
FormControl,
FormLabel,
InputGroup,
Input,
InputRightElement,
Button,
} from "@chakra-ui/react";
interface AddressInputParams {
showAddress: string;
setShowAddress: (value: string) => void;
setAddress: (value: string) => void;
setIsAddressValid: (value: boolean) => void;
bg: string;
isAddressValid: boolean;
selectedTabIndex: number;
isConnected: boolean;
appUrl: string | undefined;
isIFrameLoading: boolean;
updateAddress: () => void;
}
function AddressInput({
showAddress,
setShowAddress,
setAddress,
setIsAddressValid,
bg,
isAddressValid,
selectedTabIndex,
isConnected,
appUrl,
isIFrameLoading,
updateAddress,
}: AddressInputParams) {
return (
<FormControl>
<FormLabel>Enter Address or ENS to Impersonate</FormLabel>
<InputGroup>
<Input
placeholder="vitalik.eth"
autoComplete="off"
value={showAddress}
onChange={(e) => {
const _showAddress = e.target.value;
setShowAddress(_showAddress);
setAddress(_showAddress);
setIsAddressValid(true); // remove inValid warning when user types again
}}
bg={bg}
isInvalid={!isAddressValid}
/>
{((selectedTabIndex === 0 && isConnected) ||
(selectedTabIndex === 1 && appUrl && !isIFrameLoading)) && (
<InputRightElement width="4.5rem" mr="1rem">
<Button h="1.75rem" size="sm" onClick={updateAddress}>
Update
</Button>
</InputRightElement>
)}
</InputGroup>
</FormControl>
);
}
export default AddressInput;

View File

@@ -0,0 +1,43 @@
import {
Center,
Box,
Text,
chakra,
HStack,
Link,
Image,
} from "@chakra-ui/react";
function BrowserExtensionTab() {
return (
<Center flexDir={"column"} mt="3">
<Box w="full" fontWeight={"semibold"} fontSize={"xl"}>
<Text>
Download the browser extension from:{" "}
<chakra.a
color="blue.200"
href="https://chrome.google.com/webstore/detail/impersonator/hgihfkmoibhccfdohjdbklmmcknjjmgl"
target={"_blank"}
rel="noopener noreferrer"
>
Chrome Web Store
</chakra.a>
</Text>
</Box>
<HStack mt="2" w="full" fontSize={"lg"}>
<Text>Read more:</Text>
<Link
color="cyan.200"
fontWeight={"semibold"}
href="https://twitter.com/apoorvlathey/status/1577624123177508864"
isExternal
>
Launch Tweet
</Link>
</HStack>
<Image mt="2" src="/extension.png" />
</Center>
);
}
export default BrowserExtensionTab;

View File

@@ -0,0 +1,15 @@
import { Button } from "@chakra-ui/react";
import { CopyIcon } from "@chakra-ui/icons";
const CopyToClipboard = ({ txt }: { txt: string }) => (
<Button
onClick={() => {
navigator.clipboard.writeText(txt);
}}
size="sm"
>
<CopyIcon />
</Button>
);
export default CopyToClipboard;

View File

@@ -0,0 +1,29 @@
import { FormLabel, Tooltip, Text, Box } from "@chakra-ui/react";
import { InfoIcon } from "@chakra-ui/icons";
function AppUrlLabel() {
return (
<>
<FormLabel>dapp URL</FormLabel>
<Tooltip
label={
<>
<Text>Paste the URL of dapp you want to connect to</Text>
<Text>
Note: Some dapps might not support it, so use WalletConnect in
that case
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
</>
);
}
export default AppUrlLabel;

View File

@@ -0,0 +1,45 @@
import { GridItem, Center, Image, Text } from "@chakra-ui/react";
import { SafeDappInfo } from "../../../../types";
interface DappTileParams {
initIFrame: (_inputAppUrl?: string | undefined) => Promise<void>;
setInputAppUrl: (value: string | undefined) => void;
closeSafeApps: () => void;
dapp: SafeDappInfo;
}
function DappTile({
initIFrame,
setInputAppUrl,
closeSafeApps,
dapp,
}: DappTileParams) {
return (
<GridItem
border="2px solid"
borderColor={"gray.500"}
bg={"white"}
color={"black"}
_hover={{
cursor: "pointer",
bgColor: "gray.600",
color: "white",
}}
rounded="lg"
onClick={() => {
initIFrame(dapp.url);
setInputAppUrl(dapp.url);
closeSafeApps();
}}
>
<Center flexDir={"column"} h="100%" p="1rem">
<Image w="2rem" src={dapp.iconUrl} borderRadius="full" />
<Text mt="0.5rem" textAlign={"center"}>
{dapp.name}
</Text>
</Center>
</GridItem>
);
}
export default DappTile;

View File

@@ -0,0 +1,40 @@
import {
Center,
InputGroup,
Input,
InputRightElement,
Button,
} from "@chakra-ui/react";
import { CloseIcon } from "@chakra-ui/icons";
interface DappsSearchParams {
searchSafeDapp: string | undefined;
setSearchSafeDapp: (value: string) => void;
}
function DappsSearch({ searchSafeDapp, setSearchSafeDapp }: DappsSearchParams) {
return (
<Center pb="0.5rem">
<InputGroup maxW="30rem">
<Input
placeholder="search 🔎"
value={searchSafeDapp}
onChange={(e) => setSearchSafeDapp(e.target.value)}
/>
{searchSafeDapp && (
<InputRightElement width="3rem">
<Button
size="xs"
variant={"ghost"}
onClick={() => setSearchSafeDapp("")}
>
<CloseIcon />
</Button>
</InputRightElement>
)}
</InputGroup>
</Center>
);
}
export default DappsSearch;

View File

@@ -0,0 +1,146 @@
import { useState, useEffect } from "react";
import {
Box,
Button,
Center,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
SimpleGrid,
Spinner,
useDisclosure,
} from "@chakra-ui/react";
import axios from "axios";
import DappsSearch from "./DappsSearch";
import DappTile from "./DappTile";
import { SafeDappInfo } from "../../../../types";
interface SupportedDappsParams {
networkId: number;
initIFrame: (_inputAppUrl?: string | undefined) => Promise<void>;
setInputAppUrl: (value: string | undefined) => void;
}
function SupportedDapps({
networkId,
initIFrame,
setInputAppUrl,
}: SupportedDappsParams) {
const {
isOpen: isSafeAppsOpen,
onOpen: openSafeAapps,
onClose: closeSafeApps,
} = useDisclosure();
const [safeDapps, setSafeDapps] = useState<{
[networkId: number]: SafeDappInfo[];
}>({});
const [searchSafeDapp, setSearchSafeDapp] = useState<string>();
const [filteredSafeDapps, setFilteredSafeDapps] = useState<SafeDappInfo[]>();
useEffect(() => {
const fetchSafeDapps = async (networkId: number) => {
const response = await axios.get<SafeDappInfo[]>(
`https://safe-client.gnosis.io/v1/chains/${networkId}/safe-apps`
);
setSafeDapps((dapps) => ({
...dapps,
[networkId]: response.data.filter((d) => ![29, 11].includes(d.id)), // Filter out Transaction Builder and WalletConnect
}));
};
if (isSafeAppsOpen && !safeDapps[networkId]) {
fetchSafeDapps(networkId);
}
}, [isSafeAppsOpen, safeDapps, networkId]);
useEffect(() => {
if (safeDapps[networkId]) {
setFilteredSafeDapps(
safeDapps[networkId].filter((dapp) => {
if (!searchSafeDapp) return true;
return (
dapp.name
.toLowerCase()
.indexOf(searchSafeDapp.toLocaleLowerCase()) !== -1 ||
dapp.url
.toLowerCase()
.indexOf(searchSafeDapp.toLocaleLowerCase()) !== -1
);
})
);
} else {
setFilteredSafeDapps(undefined);
}
}, [safeDapps, networkId, searchSafeDapp]);
return (
<>
<Box pb="0.5rem">
<Button size="sm" onClick={openSafeAapps}>
Supported dapps
</Button>
</Box>
<Modal isOpen={isSafeAppsOpen} onClose={closeSafeApps} isCentered>
<ModalOverlay bg="none" backdropFilter="auto" backdropBlur="3px" />
<ModalContent
minW={{
base: 0,
sm: "30rem",
md: "40rem",
lg: "60rem",
}}
>
<ModalHeader>Select a dapp</ModalHeader>
<ModalCloseButton />
<ModalBody maxH="30rem" overflow={"clip"}>
{(!safeDapps || !safeDapps[networkId]) && (
<Center py="3rem" w="100%">
<Spinner />
</Center>
)}
<Box pb="2rem" px={{ base: 0, md: "2rem" }}>
{safeDapps && safeDapps[networkId] && (
<DappsSearch
searchSafeDapp={searchSafeDapp}
setSearchSafeDapp={setSearchSafeDapp}
/>
)}
<Box
minH="30rem"
maxH="30rem"
overflow="scroll"
overflowX="auto"
overflowY="auto"
>
<SimpleGrid
pt="1rem"
columns={{ base: 2, md: 3, lg: 4 }}
gap={6}
>
{filteredSafeDapps &&
filteredSafeDapps.map((dapp, i) => (
<DappTile
key={i}
initIFrame={initIFrame}
setInputAppUrl={setInputAppUrl}
closeSafeApps={closeSafeApps}
dapp={dapp}
/>
))}
</SimpleGrid>
</Box>
</Box>
</ModalBody>
</ModalContent>
</Modal>
</>
);
}
export default SupportedDapps;

View File

@@ -0,0 +1,96 @@
import {
Button,
Box,
Center,
Spacer,
HStack,
FormControl,
Input,
} from "@chakra-ui/react";
import SupportedDapps from "./SupportedDapps";
import AppUrlLabel from "./AppUrlLabel";
interface IFrameConnectTabParams {
networkId: number;
initIFrame: (_inputAppUrl?: string | undefined) => Promise<void>;
inputAppUrl: string | undefined;
setInputAppUrl: (value: string | undefined) => void;
appUrl: string | undefined;
bg: string;
isIFrameLoading: boolean;
setIsIFrameLoading: (value: boolean) => void;
iframeKey: number;
iframeRef: React.RefObject<HTMLIFrameElement> | null;
}
function IFrameConnectTab({
networkId,
initIFrame,
setInputAppUrl,
inputAppUrl,
bg,
isIFrameLoading,
appUrl,
iframeKey,
iframeRef,
setIsIFrameLoading,
}: IFrameConnectTabParams) {
return (
<>
<FormControl my={4}>
<HStack>
<AppUrlLabel />
<Spacer />
<SupportedDapps
networkId={networkId}
initIFrame={initIFrame}
setInputAppUrl={setInputAppUrl}
/>
</HStack>
<Input
placeholder="https://app.uniswap.org/"
aria-label="dapp-url"
autoComplete="off"
value={inputAppUrl}
onChange={(e) => setInputAppUrl(e.target.value)}
bg={bg}
/>
</FormControl>
<Center>
<Button onClick={() => initIFrame()} isLoading={isIFrameLoading}>
Connect
</Button>
</Center>
<Center
mt="1rem"
ml={{ base: "-385", sm: "-315", md: "-240", lg: "-60" }}
px={{ base: "10rem", lg: 0 }}
w="70rem"
>
{appUrl && (
<Box
as="iframe"
w={{
base: "22rem",
sm: "45rem",
md: "55rem",
lg: "1500rem",
}}
h={{ base: "33rem", md: "35rem", lg: "38rem" }}
title="app"
src={appUrl}
key={iframeKey}
borderWidth="1px"
borderStyle={"solid"}
borderColor="white"
bg="white"
ref={iframeRef}
onLoad={() => setIsIFrameLoading(false)}
/>
)}
</Center>
</>
);
}
export default IFrameConnectTab;

View File

@@ -0,0 +1,62 @@
import { Box } from "@chakra-ui/react";
import { Select as RSelect, SingleValue } from "chakra-react-select";
import { SelectedNetworkOption } from "../../types";
interface NetworkOption {
name: string;
rpcs: string[];
chainId: number;
}
interface NetworkInputParams {
primaryNetworkOptions: NetworkOption[];
secondaryNetworkOptions: NetworkOption[];
selectedNetworkOption: SingleValue<SelectedNetworkOption>;
setSelectedNetworkOption: (value: SingleValue<SelectedNetworkOption>) => void;
}
function NetworkInput({
primaryNetworkOptions,
secondaryNetworkOptions,
selectedNetworkOption,
setSelectedNetworkOption,
}: NetworkInputParams) {
return (
<Box mt={4} cursor="pointer">
<RSelect
options={[
{
label: "",
options: primaryNetworkOptions.map((network) => ({
label: network.name,
value: network.chainId,
})),
},
{
label: "",
options: secondaryNetworkOptions.map((network) => ({
label: network.name,
value: network.chainId,
})),
},
]}
value={selectedNetworkOption}
onChange={setSelectedNetworkOption}
placeholder="Select chain..."
size="md"
tagVariant="solid"
chakraStyles={{
groupHeading: (provided, state) => ({
...provided,
h: "1px",
borderTop: "1px solid white",
}),
}}
closeMenuOnSelect
useBasicStyles
/>
</Box>
);
}
export default NetworkInput;

View File

@@ -0,0 +1,41 @@
import { Center, HStack } from "@chakra-ui/react";
import Tab from "./Tab";
const tabs = ["WalletConnect", "iFrame", "Extension"];
interface TabsSelectParams {
selectedTabIndex: number;
setSelectedTabIndex: (value: number) => void;
}
function TabsSelect({
selectedTabIndex,
setSelectedTabIndex,
}: TabsSelectParams) {
return (
<Center flexDir="column">
<HStack
mt="1rem"
minH="3rem"
px="1.5rem"
spacing={"8"}
background="gray.700"
borderRadius="xl"
>
{tabs.map((t, i) => (
<Tab
key={i}
tabIndex={i}
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
isNew={i === 2}
>
{t}
</Tab>
))}
</HStack>
</Center>
);
}
export default TabsSelect;

View File

@@ -0,0 +1,84 @@
import {
Input,
Button,
Box,
Text,
Popover,
PopoverTrigger,
PopoverContent,
Tooltip,
HStack,
chakra,
ListItem,
List,
useDisclosure,
} from "@chakra-ui/react";
import { SettingsIcon, InfoIcon } from "@chakra-ui/icons";
interface TenderlySettingsParams {
tenderlyForkId: string;
setTenderlyForkId: (value: string) => void;
}
function TenderlySettings({
tenderlyForkId,
setTenderlyForkId,
}: TenderlySettingsParams) {
const { onOpen, onClose, isOpen } = useDisclosure();
return (
<Popover
placement="bottom-start"
isOpen={isOpen}
onOpen={onOpen}
onClose={onClose}
>
<PopoverTrigger>
<Box>
<Button>
<SettingsIcon
transition="900ms rotate ease-in-out"
transform={isOpen ? "rotate(33deg)" : "rotate(0deg)"}
/>
</Button>
</Box>
</PopoverTrigger>
<PopoverContent border={0} boxShadow="xl" rounded="xl" overflowY="auto">
<Box px="1rem" py="1rem">
<HStack>
<Text>(optional) Tenderly Fork Id:</Text>
<Tooltip
label={
<>
<Text>Simulate sending transactions on forked node.</Text>
<chakra.hr bg="gray.400" />
<List>
<ListItem>
Create a fork on Tenderly and grab it's id from the URL.
</ListItem>
</List>
</>
}
hasArrow
placement="top"
>
<InfoIcon />
</Tooltip>
</HStack>
<Input
mt="0.5rem"
aria-label="fork-rpc"
placeholder="xxxx-xxxx-xxxx-xxxx"
autoComplete="off"
value={tenderlyForkId}
onChange={(e) => {
setTenderlyForkId(e.target.value);
}}
/>
</Box>
</PopoverContent>
</Popover>
);
}
export default TenderlySettings;

View File

@@ -0,0 +1,123 @@
import {
Box,
Flex,
HStack,
Text,
Heading,
Tooltip,
Td,
Collapse,
useDisclosure,
Button,
Table,
Thead,
Tr,
Th,
Tbody,
} from "@chakra-ui/react";
import {
InfoIcon,
ChevronDownIcon,
ChevronUpIcon,
DeleteIcon,
} from "@chakra-ui/icons";
import CopyToClipboard from "./CopyToClipboard";
import { TxnDataType } from "../../types";
const slicedText = (txt: string) => {
return txt.length > 6
? `${txt.slice(0, 4)}...${txt.slice(txt.length - 2, txt.length)}`
: txt;
};
const TD = ({ txt }: { txt: string }) => (
<Td>
<HStack>
<Tooltip label={txt} hasArrow placement="top">
<Text>{slicedText(txt)}</Text>
</Tooltip>
<CopyToClipboard txt={txt} />
</HStack>
</Td>
);
interface TransactionRequestsParams {
sendTxnData: TxnDataType[];
setSendTxnData: (value: TxnDataType[]) => void;
}
function TransactionRequests({
sendTxnData,
setSendTxnData,
}: TransactionRequestsParams) {
const { isOpen: tableIsOpen, onToggle: tableOnToggle } = useDisclosure();
return (
<Box
minW={["0", "0", "2xl", "2xl"]}
overflowX={"auto"}
mt="2rem"
pt="0.5rem"
pl="1rem"
border={"1px solid"}
borderColor={"white.800"}
rounded="lg"
>
<Flex py="2" pl="2" pr="4">
<HStack cursor={"pointer"} onClick={tableOnToggle}>
<Text fontSize={"xl"}>
{tableIsOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
</Text>
<Heading size={"md"}>eth_sendTransactions</Heading>
<Tooltip
label={
<>
<Text>
"eth_sendTransaction" requests by the dApp are shown here
(latest on top)
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
</HStack>
<Flex flex="1" />
{sendTxnData.length > 0 && (
<Button onClick={() => setSendTxnData([])}>
<DeleteIcon />
<Text pl="0.5rem">Clear</Text>
</Button>
)}
</Flex>
<Collapse in={tableIsOpen} animateOpacity>
<Table variant="simple">
<Thead>
<Tr>
<Th>from</Th>
<Th>to</Th>
<Th>data</Th>
<Th>value</Th>
</Tr>
</Thead>
<Tbody>
{sendTxnData.map((d) => (
<Tr key={d.id}>
<TD txt={d.from} />
<TD txt={d.to} />
<TD txt={d.data} />
<TD txt={d.value} />
</Tr>
))}
</Tbody>
</Table>
</Collapse>
</Box>
);
}
export default TransactionRequests;

View File

@@ -0,0 +1,38 @@
import { Box, Text, Button, VStack, Avatar, Link } from "@chakra-ui/react";
import { SessionTypes } from "@walletconnect/types";
interface ConnectionDetailsParams {
web3WalletSession: SessionTypes.Struct;
killSession: () => void;
}
function ConnectionDetails({
web3WalletSession,
killSession,
}: ConnectionDetailsParams) {
return (
<>
<Box mt={4} fontSize={24} fontWeight="semibold">
Connected To:
</Box>
<VStack>
<Avatar src={web3WalletSession.peer?.metadata?.icons[0]} />
<Text fontWeight="bold">{web3WalletSession.peer?.metadata?.name}</Text>
<Text fontSize="sm">
{web3WalletSession.peer?.metadata?.description}
</Text>
<Link
href={web3WalletSession.peer?.metadata?.url}
textDecor="underline"
>
{web3WalletSession.peer?.metadata?.url}
</Link>
<Box pt={6}>
<Button onClick={() => killSession()}>Disconnect </Button>
</Box>
</VStack>
</>
);
}
export default ConnectionDetails;

View File

@@ -0,0 +1,33 @@
import { Box, Text, Button, VStack, Avatar, Link } from "@chakra-ui/react";
import { IClientMeta } from "@walletconnect/legacy-types";
interface LegacyConnectionDetailsParams {
legacyPeerMeta: IClientMeta;
killSession: () => void;
}
function LegacyConnectionDetails({
legacyPeerMeta,
killSession,
}: LegacyConnectionDetailsParams) {
return (
<>
<Box mt={4} fontSize={24} fontWeight="semibold">
Connected To:
</Box>
<VStack>
<Avatar src={legacyPeerMeta.icons[0]} />
<Text fontWeight="bold">{legacyPeerMeta.name}</Text>
<Text fontSize="sm">{legacyPeerMeta.description}</Text>
<Link href={legacyPeerMeta.url} textDecor="underline">
{legacyPeerMeta.url}
</Link>
<Box pt={6}>
<Button onClick={() => killSession()}>Disconnect </Button>
</Box>
</VStack>
</>
);
}
export default LegacyConnectionDetails;

View File

@@ -0,0 +1,39 @@
import {
Box,
Center,
Button,
VStack,
CircularProgress,
} from "@chakra-ui/react";
interface LoadingParams {
isConnected: boolean;
setLoading: (value: boolean) => void;
reset: (persistUri?: boolean) => void;
}
function Loading({ isConnected, setLoading, reset }: LoadingParams) {
return (
<Center>
<VStack>
<Box>
<CircularProgress isIndeterminate />
</Box>
{!isConnected && (
<Box pt={6}>
<Button
onClick={() => {
setLoading(false);
reset(true);
}}
>
Stop Loading
</Button>
</Box>
)}
</VStack>
</Center>
);
}
export default Loading;

View File

@@ -0,0 +1,57 @@
import {
FormControl,
HStack,
FormLabel,
Tooltip,
Box,
Text,
Input,
} from "@chakra-ui/react";
import { InfoIcon } from "@chakra-ui/icons";
interface URIInputParams {
uri: string;
setUri: (value: string) => void;
bg: string;
isConnected: boolean;
}
function URIInput({ uri, setUri, bg, isConnected }: URIInputParams) {
return (
<FormControl my={4}>
<HStack>
<FormLabel>WalletConnect URI</FormLabel>
<Tooltip
label={
<>
<Text>Visit any dApp and select WalletConnect.</Text>
<Text>
Click "Copy to Clipboard" beneath the QR code, and paste it
here.
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
</HStack>
<Box>
<Input
placeholder="wc:xyz123"
aria-label="uri"
autoComplete="off"
value={uri}
onChange={(e) => setUri(e.target.value)}
bg={bg}
isDisabled={isConnected}
/>
</Box>
</FormControl>
);
}
export default URIInput;

View File

@@ -0,0 +1,67 @@
import { Center, Button } from "@chakra-ui/react";
import { IClientMeta } from "@walletconnect/legacy-types";
import { SessionTypes } from "@walletconnect/types";
import ConnectionDetails from "./ConnectionDetails";
import LegacyConnectionDetails from "./LegacyConnectionDetails";
import Loading from "./Loading";
import URIInput from "./URIInput";
interface WalletConnectTabParams {
uri: string;
setUri: (value: string) => void;
bg: string;
isConnected: boolean;
initWalletConnect: () => void;
loading: boolean;
setLoading: (value: boolean) => void;
reset: (persistUri?: boolean) => void;
killSession: () => void;
legacyPeerMeta: IClientMeta | undefined;
web3WalletSession: SessionTypes.Struct | undefined;
}
function WalletConnectTab({
uri,
setUri,
bg,
isConnected,
initWalletConnect,
loading,
setLoading,
reset,
legacyPeerMeta,
killSession,
web3WalletSession,
}: WalletConnectTabParams) {
return (
<>
<URIInput uri={uri} setUri={setUri} bg={bg} isConnected={isConnected} />
<Center>
<Button onClick={initWalletConnect} isDisabled={isConnected}>
Connect
</Button>
</Center>
{loading && (
<Loading
isConnected={isConnected}
setLoading={setLoading}
reset={reset}
/>
)}
{legacyPeerMeta && isConnected && (
<LegacyConnectionDetails
legacyPeerMeta={legacyPeerMeta}
killSession={killSession}
/>
)}
{web3WalletSession && isConnected && (
<ConnectionDetails
web3WalletSession={web3WalletSession}
killSession={killSession}
/>
)}
</>
);
}
export default WalletConnectTab;

View File

@@ -1,61 +1,14 @@
import { useState, useEffect } from "react";
import {
Container,
InputGroup,
Input,
InputRightElement,
FormControl,
useColorMode,
FormLabel,
Button,
Box,
Avatar,
Text,
Link,
VStack,
useToast,
CircularProgress,
Center,
Spacer,
Flex,
useDisclosure,
Popover,
PopoverTrigger,
PopoverContent,
Tooltip,
HStack,
chakra,
ListItem,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Heading,
Collapse,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
SimpleGrid,
GridItem,
Image,
Spinner,
List,
} from "@chakra-ui/react";
import {
SettingsIcon,
InfoIcon,
ChevronDownIcon,
ChevronUpIcon,
CopyIcon,
DeleteIcon,
CloseIcon,
} from "@chakra-ui/icons";
import { Select as RSelect, SingleValue } from "chakra-react-select";
import { SingleValue } from "chakra-react-select";
// WC v1
import LegacySignClient from "@walletconnect/client";
import { IClientMeta } from "@walletconnect/legacy-types";
@@ -68,19 +21,15 @@ import { ethers } from "ethers";
import axios from "axios";
import networksList from "evm-rpcs-list";
import { useSafeInject } from "../../contexts/SafeInjectContext";
import Tab from "./Tab";
interface SafeDappInfo {
id: number;
url: string;
name: string;
iconUrl: string;
}
interface SelectedNetworkOption {
label: string;
value: number;
}
import TenderlySettings from "./TenderlySettings";
import AddressInput from "./AddressInput";
import { SelectedNetworkOption, TxnDataType } from "../../types";
import NetworkInput from "./NetworkInput";
import TabsSelect from "./TabsSelect";
import WalletConnectTab from "./WalletConnectTab";
import IFrameConnectTab from "./IFrameConnectTab";
import BrowserExtensionTab from "./BrowserExtensionTab";
import TransactionRequests from "./TransactionRequests";
const WCMetadata = {
name: "Impersonator",
@@ -122,34 +71,6 @@ const allNetworksOptions = [
...secondaryNetworkOptions,
];
const slicedText = (txt: string) => {
return txt.length > 6
? `${txt.slice(0, 4)}...${txt.slice(txt.length - 2, txt.length)}`
: txt;
};
const CopyToClipboard = ({ txt }: { txt: string }) => (
<Button
onClick={() => {
navigator.clipboard.writeText(txt);
}}
size="sm"
>
<CopyIcon />
</Button>
);
const TD = ({ txt }: { txt: string }) => (
<Td>
<HStack>
<Tooltip label={txt} hasArrow placement="top">
<Text>{slicedText(txt)}</Text>
</Tooltip>
<CopyToClipboard txt={txt} />
</HStack>
</Td>
);
function Body() {
const { colorMode } = useColorMode();
const bgColor = { light: "white", dark: "gray.700" };
@@ -172,13 +93,6 @@ function Body() {
}
}
const toast = useToast();
const { onOpen, onClose, isOpen } = useDisclosure();
const { isOpen: tableIsOpen, onToggle: tableOnToggle } = useDisclosure();
const {
isOpen: isSafeAppsOpen,
onOpen: openSafeAapps,
onClose: closeSafeApps,
} = useDisclosure();
const {
setAddress: setIFrameAddress,
@@ -211,29 +125,16 @@ function Body() {
const [isConnected, setIsConnected] = useState(false);
const [loading, setLoading] = useState(false);
const tabs = ["WalletConnect", "iFrame", "Extension"];
const [selectedTabIndex, setSelectedTabIndex] = useState(urlFromURL ? 1 : 0);
const [isIFrameLoading, setIsIFrameLoading] = useState(false);
const [safeDapps, setSafeDapps] = useState<{
[networkId: number]: SafeDappInfo[];
}>({});
const [searchSafeDapp, setSearchSafeDapp] = useState<string>();
const [filteredSafeDapps, setFilteredSafeDapps] = useState<SafeDappInfo[]>();
const [inputAppUrl, setInputAppUrl] = useState<string | undefined>(
urlFromURL ?? undefined
);
const [iframeKey, setIframeKey] = useState(0); // hacky way to reload iframe when key changes
const [tenderlyForkId, setTenderlyForkId] = useState("");
const [sendTxnData, setSendTxnData] = useState<
{
id: number;
from: string;
to: string;
data: string;
value: string;
}[]
>([]);
const [sendTxnData, setSendTxnData] = useState<TxnDataType[]>([]);
useEffect(() => {
// WC V1
@@ -352,43 +253,6 @@ function Body() {
// eslint-disable-next-line
}, [latestTransaction, tenderlyForkId]);
useEffect(() => {
const fetchSafeDapps = async (networkId: number) => {
const response = await axios.get<SafeDappInfo[]>(
`https://safe-client.gnosis.io/v1/chains/${networkId}/safe-apps`
);
setSafeDapps((dapps) => ({
...dapps,
[networkId]: response.data.filter((d) => ![29, 11].includes(d.id)), // Filter out Transaction Builder and WalletConnect
}));
};
if (isSafeAppsOpen && !safeDapps[networkId]) {
fetchSafeDapps(networkId);
}
}, [isSafeAppsOpen, safeDapps, networkId]);
useEffect(() => {
if (safeDapps[networkId]) {
setFilteredSafeDapps(
safeDapps[networkId].filter((dapp) => {
if (!searchSafeDapp) return true;
return (
dapp.name
.toLowerCase()
.indexOf(searchSafeDapp.toLocaleLowerCase()) !== -1 ||
dapp.url
.toLowerCase()
.indexOf(searchSafeDapp.toLocaleLowerCase()) !== -1
);
})
);
} else {
setFilteredSafeDapps(undefined);
}
}, [safeDapps, networkId, searchSafeDapp]);
const initWeb3Wallet = async (
onlyIfActiveSessions?: boolean,
_showAddress?: string
@@ -866,539 +730,76 @@ function Body() {
<Container my="16" minW={["0", "0", "2xl", "2xl"]}>
<Flex>
<Spacer flex="1" />
<Popover
placement="bottom-start"
isOpen={isOpen}
onOpen={onOpen}
onClose={onClose}
>
<PopoverTrigger>
<Box>
<Button>
<SettingsIcon
transition="900ms rotate ease-in-out"
transform={isOpen ? "rotate(33deg)" : "rotate(0deg)"}
/>
</Button>
</Box>
</PopoverTrigger>
<PopoverContent
border={0}
boxShadow="xl"
rounded="xl"
overflowY="auto"
>
<Box px="1rem" py="1rem">
<HStack>
<Text>(optional) Tenderly Fork Id:</Text>
<Tooltip
label={
<>
<Text>Simulate sending transactions on forked node.</Text>
<chakra.hr bg="gray.400" />
<List>
<ListItem>
Create a fork on Tenderly and grab it's id from the
URL.
</ListItem>
</List>
</>
}
hasArrow
placement="top"
>
<InfoIcon />
</Tooltip>
</HStack>
<Input
mt="0.5rem"
aria-label="fork-rpc"
placeholder="xxxx-xxxx-xxxx-xxxx"
autoComplete="off"
value={tenderlyForkId}
onChange={(e) => {
setTenderlyForkId(e.target.value);
}}
/>
</Box>
</PopoverContent>
</Popover>
</Flex>
<FormControl>
<FormLabel>Enter Address or ENS to Impersonate</FormLabel>
<InputGroup>
<Input
placeholder="vitalik.eth"
autoComplete="off"
value={showAddress}
onChange={(e) => {
const _showAddress = e.target.value;
setShowAddress(_showAddress);
setAddress(_showAddress);
setIsAddressValid(true); // remove inValid warning when user types again
}}
bg={bgColor[colorMode]}
isInvalid={!isAddressValid}
/>
{((selectedTabIndex === 0 && isConnected) ||
(selectedTabIndex === 1 && appUrl && !isIFrameLoading)) && (
<InputRightElement width="4.5rem" mr="1rem">
<Button h="1.75rem" size="sm" onClick={updateAddress}>
Update
</Button>
</InputRightElement>
)}
</InputGroup>
</FormControl>
<Box mt={4} cursor="pointer">
<RSelect
options={[
{
label: "",
options: primaryNetworkOptions.map((network) => ({
label: network.name,
value: network.chainId,
})),
},
{
label: "",
options: secondaryNetworkOptions.map((network) => ({
label: network.name,
value: network.chainId,
})),
},
]}
value={selectedNetworkOption}
onChange={setSelectedNetworkOption}
placeholder="Select chain..."
size="md"
tagVariant="solid"
chakraStyles={{
groupHeading: (provided, state) => ({
...provided,
h: "1px",
borderTop: "1px solid white",
}),
}}
closeMenuOnSelect
useBasicStyles
<TenderlySettings
tenderlyForkId={tenderlyForkId}
setTenderlyForkId={setTenderlyForkId}
/>
</Box>
<Center flexDir="column">
<HStack
mt="1rem"
minH="3rem"
px="1.5rem"
spacing={"8"}
background="gray.700"
borderRadius="xl"
>
{tabs.map((t, i) => (
<Tab
key={i}
tabIndex={i}
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
isNew={i === 2}
>
{t}
</Tab>
))}
</HStack>
</Center>
</Flex>
<AddressInput
showAddress={showAddress}
setShowAddress={setShowAddress}
setAddress={setAddress}
setIsAddressValid={setIsAddressValid}
bg={bgColor[colorMode]}
isAddressValid={isAddressValid}
selectedTabIndex={selectedTabIndex}
isConnected={isConnected}
appUrl={appUrl}
isIFrameLoading={isIFrameLoading}
updateAddress={updateAddress}
/>
<NetworkInput
primaryNetworkOptions={primaryNetworkOptions}
secondaryNetworkOptions={secondaryNetworkOptions}
selectedNetworkOption={selectedNetworkOption}
setSelectedNetworkOption={setSelectedNetworkOption}
/>
<TabsSelect
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
/>
{(() => {
switch (selectedTabIndex) {
case 0:
return (
<>
<FormControl my={4}>
<HStack>
<FormLabel>WalletConnect URI</FormLabel>
<Tooltip
label={
<>
<Text>Visit any dApp and select WalletConnect.</Text>
<Text>
Click "Copy to Clipboard" beneath the QR code, and
paste it here.
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
</HStack>
<Box>
<Input
placeholder="wc:xyz123"
aria-label="uri"
autoComplete="off"
value={uri}
onChange={(e) => setUri(e.target.value)}
bg={bgColor[colorMode]}
isDisabled={isConnected}
/>
</Box>
</FormControl>
<Center>
<Button onClick={initWalletConnect} isDisabled={isConnected}>
Connect
</Button>
</Center>
{loading && (
<Center>
<VStack>
<Box>
<CircularProgress isIndeterminate />
</Box>
{!isConnected && (
<Box pt={6}>
<Button
onClick={() => {
setLoading(false);
reset(true);
}}
>
Stop Loading
</Button>
</Box>
)}
</VStack>
</Center>
)}
{legacyPeerMeta && isConnected && (
<>
<Box mt={4} fontSize={24} fontWeight="semibold">
Connected To:
</Box>
<VStack>
<Avatar src={legacyPeerMeta.icons[0]} />
<Text fontWeight="bold">{legacyPeerMeta.name}</Text>
<Text fontSize="sm">{legacyPeerMeta.description}</Text>
<Link href={legacyPeerMeta.url} textDecor="underline">
{legacyPeerMeta.url}
</Link>
<Box pt={6}>
<Button onClick={() => killSession()}>
Disconnect
</Button>
</Box>
</VStack>
</>
)}
{web3WalletSession && isConnected && (
<>
<Box mt={4} fontSize={24} fontWeight="semibold">
Connected To:
</Box>
<VStack>
<Avatar
src={web3WalletSession.peer?.metadata?.icons[0]}
/>
<Text fontWeight="bold">
{web3WalletSession.peer?.metadata?.name}
</Text>
<Text fontSize="sm">
{web3WalletSession.peer?.metadata?.description}
</Text>
<Link
href={web3WalletSession.peer?.metadata?.url}
textDecor="underline"
>
{web3WalletSession.peer?.metadata?.url}
</Link>
<Box pt={6}>
<Button onClick={() => killSession()}>
Disconnect
</Button>
</Box>
</VStack>
</>
)}
</>
<WalletConnectTab
uri={uri}
setUri={setUri}
bg={bgColor[colorMode]}
isConnected={isConnected}
initWalletConnect={initWalletConnect}
loading={loading}
setLoading={setLoading}
reset={reset}
legacyPeerMeta={legacyPeerMeta}
killSession={killSession}
web3WalletSession={web3WalletSession}
/>
);
case 1:
return (
<>
<FormControl my={4}>
<HStack>
<FormLabel>dapp URL</FormLabel>
<Tooltip
label={
<>
<Text>
Paste the URL of dapp you want to connect to
</Text>
<Text>
Note: Some dapps might not support it, so use
WalletConnect in that case
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
<Spacer />
<Box pb="0.5rem">
<Button size="sm" onClick={openSafeAapps}>
Supported dapps
</Button>
</Box>
<Modal
isOpen={isSafeAppsOpen}
onClose={closeSafeApps}
isCentered
>
<ModalOverlay
bg="none"
backdropFilter="auto"
backdropBlur="3px"
/>
<ModalContent
minW={{
base: 0,
sm: "30rem",
md: "40rem",
lg: "60rem",
}}
>
<ModalHeader>Select a dapp</ModalHeader>
<ModalCloseButton />
<ModalBody maxH="30rem" overflow={"clip"}>
{(!safeDapps || !safeDapps[networkId]) && (
<Center py="3rem" w="100%">
<Spinner />
</Center>
)}
<Box pb="2rem" px={{ base: 0, md: "2rem" }}>
{safeDapps && safeDapps[networkId] && (
<Center pb="0.5rem">
<InputGroup maxW="30rem">
<Input
placeholder="search 🔎"
value={searchSafeDapp}
onChange={(e) =>
setSearchSafeDapp(e.target.value)
}
/>
{searchSafeDapp && (
<InputRightElement width="3rem">
<Button
size="xs"
variant={"ghost"}
onClick={() => setSearchSafeDapp("")}
>
<CloseIcon />
</Button>
</InputRightElement>
)}
</InputGroup>
</Center>
)}
<Box
minH="30rem"
maxH="30rem"
overflow="scroll"
overflowX="auto"
overflowY="auto"
>
<SimpleGrid
pt="1rem"
columns={{ base: 2, md: 3, lg: 4 }}
gap={6}
>
{filteredSafeDapps &&
filteredSafeDapps.map((dapp, i) => (
<GridItem
key={i}
border="2px solid"
borderColor={"gray.500"}
_hover={{
cursor: "pointer",
bgColor: "gray.600",
}}
rounded="lg"
onClick={() => {
initIFrame(dapp.url);
setInputAppUrl(dapp.url);
closeSafeApps();
}}
>
<Center
flexDir={"column"}
h="100%"
p="1rem"
>
<Image
w="2rem"
src={dapp.iconUrl}
borderRadius="full"
/>
<Text mt="0.5rem" textAlign={"center"}>
{dapp.name}
</Text>
</Center>
</GridItem>
))}
</SimpleGrid>
</Box>
</Box>
</ModalBody>
</ModalContent>
</Modal>
</HStack>
<Input
placeholder="https://app.uniswap.org/"
aria-label="dapp-url"
autoComplete="off"
value={inputAppUrl}
onChange={(e) => setInputAppUrl(e.target.value)}
bg={bgColor[colorMode]}
/>
</FormControl>
<Center>
<Button
onClick={() => initIFrame()}
isLoading={isIFrameLoading}
>
Connect
</Button>
</Center>
<Center
mt="1rem"
ml={{ base: "-385", sm: "-315", md: "-240", lg: "-60" }}
px={{ base: "10rem", lg: 0 }}
w="70rem"
>
{appUrl && (
<Box
as="iframe"
w={{
base: "22rem",
sm: "45rem",
md: "55rem",
lg: "1500rem",
}}
h={{ base: "33rem", md: "35rem", lg: "38rem" }}
title="app"
src={appUrl}
key={iframeKey}
borderWidth="1px"
borderStyle={"solid"}
borderColor="white"
bg="white"
ref={iframeRef}
onLoad={() => setIsIFrameLoading(false)}
/>
)}
</Center>
</>
<IFrameConnectTab
networkId={networkId}
initIFrame={initIFrame}
setInputAppUrl={setInputAppUrl}
inputAppUrl={inputAppUrl}
bg={bgColor[colorMode]}
isIFrameLoading={isIFrameLoading}
appUrl={appUrl}
iframeKey={iframeKey}
iframeRef={iframeRef}
setIsIFrameLoading={setIsIFrameLoading}
/>
);
case 2:
return (
<Center flexDir={"column"} mt="3">
<Box w="full" fontWeight={"semibold"} fontSize={"xl"}>
<Text>
Download the browser extension from:{" "}
<chakra.a
color="blue.200"
href="https://chrome.google.com/webstore/detail/impersonator/hgihfkmoibhccfdohjdbklmmcknjjmgl"
target={"_blank"}
rel="noopener noreferrer"
>
Chrome Web Store
</chakra.a>
</Text>
</Box>
<HStack mt="2" w="full" fontSize={"lg"}>
<Text>Read more:</Text>
<Link
color="cyan.200"
fontWeight={"semibold"}
href="https://twitter.com/apoorvlathey/status/1577624123177508864"
isExternal
>
Launch Tweet
</Link>
</HStack>
<Image mt="2" src="/extension.png" />
</Center>
);
return <BrowserExtensionTab />;
}
})()}
<Center>
<Box
minW={["0", "0", "2xl", "2xl"]}
overflowX={"auto"}
mt="2rem"
pt="0.5rem"
pl="1rem"
border={"1px solid"}
borderColor={"white.800"}
rounded="lg"
>
<Flex py="2" pl="2" pr="4">
<HStack cursor={"pointer"} onClick={tableOnToggle}>
<Text fontSize={"xl"}>
{tableIsOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
</Text>
<Heading size={"md"}>eth_sendTransactions</Heading>
<Tooltip
label={
<>
<Text>
"eth_sendTransaction" requests by the dApp are shown here
(latest on top)
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
</HStack>
<Flex flex="1" />
{sendTxnData.length > 0 && (
<Button onClick={() => setSendTxnData([])}>
<DeleteIcon />
<Text pl="0.5rem">Clear</Text>
</Button>
)}
</Flex>
<Collapse in={tableIsOpen} animateOpacity>
<Table variant="simple">
<Thead>
<Tr>
<Th>from</Th>
<Th>to</Th>
<Th>data</Th>
<Th>value</Th>
</Tr>
</Thead>
<Tbody>
{sendTxnData.map((d) => (
<Tr key={d.id}>
<TD txt={d.from} />
<TD txt={d.to} />
<TD txt={d.data} />
<TD txt={d.value} />
</Tr>
))}
</Tbody>
</Table>
</Collapse>
</Box>
<TransactionRequests
sendTxnData={sendTxnData}
setSendTxnData={setSendTxnData}
/>
</Center>
</Container>
);

View File

@@ -1,5 +1,27 @@
import { BigNumberish, BytesLike } from "ethers";
export interface SelectedNetworkOption {
label: string;
value: number;
}
export interface SafeDappInfo {
id: number;
url: string;
name: string;
iconUrl: string;
}
export interface TxnDataType {
id: number;
from: string;
to: string;
data: string;
value: string;
}
// ======= iFrame Provider ======
export declare const INTERFACE_MESSAGES: {
readonly ENV_INFO: "ENV_INFO";
readonly ON_SAFE_INFO: "ON_SAFE_INFO";