feat: move to nextjs

This commit is contained in:
apoorvlathey
2024-05-07 00:18:18 +10:00
parent 602ae4389e
commit 032785a316
49 changed files with 9867 additions and 8425 deletions

26
components/Analytics.tsx Normal file
View File

@@ -0,0 +1,26 @@
import Script from "next/script";
const GA_ID = "G-QFNMM9LXBY";
export const Analytics = () => {
return (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
/>
<Script
id="google-analytics"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_ID}');
`,
}}
/>
</>
);
};

View File

@@ -0,0 +1,161 @@
import { useState, useEffect } from "react";
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
HStack,
ModalCloseButton,
ModalBody,
Text,
Input,
Center,
Button,
Box,
} from "@chakra-ui/react";
import { DeleteIcon } from "@chakra-ui/icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSave } from "@fortawesome/free-solid-svg-icons";
import { slicedText } from "../../TransactionRequests";
const STORAGE_KEY = "address-book";
interface SavedAddressInfo {
address: string;
label: string;
}
interface AddressBookParams {
isAddressBookOpen: boolean;
closeAddressBook: () => void;
showAddress: string;
setShowAddress: (value: string) => void;
setAddress: (value: string) => void;
}
function AddressBook({
isAddressBookOpen,
closeAddressBook,
showAddress,
setShowAddress,
setAddress,
}: AddressBookParams) {
const [newAddressInput, setNewAddressInput] = useState<string>("");
const [newLableInput, setNewLabelInput] = useState<string>("");
const [savedAddresses, setSavedAddresses] = useState<SavedAddressInfo[]>([]);
useEffect(() => {
setSavedAddresses(JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]"));
}, []);
useEffect(() => {
setNewAddressInput(showAddress);
}, [showAddress]);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedAddresses));
}, [savedAddresses]);
// reset label when modal is reopened
useEffect(() => {
setNewLabelInput("");
}, [isAddressBookOpen]);
return (
<Modal isOpen={isAddressBookOpen} onClose={closeAddressBook} isCentered>
<ModalOverlay bg="none" backdropFilter="auto" backdropBlur="5px" />
<ModalContent
minW={{
base: 0,
sm: "30rem",
md: "40rem",
lg: "60rem",
}}
pb="6"
bg={"brand.lightBlack"}
>
<ModalHeader>Address Book</ModalHeader>
<ModalCloseButton />
<ModalBody>
<HStack>
<Input
placeholder="address / ens"
value={newAddressInput}
onChange={(e) => setNewAddressInput(e.target.value)}
/>
<Input
placeholder="label"
value={newLableInput}
onChange={(e) => setNewLabelInput(e.target.value)}
/>
</HStack>
<Center mt="3">
<Button
colorScheme={"blue"}
isDisabled={
newAddressInput.length === 0 || newLableInput.length === 0
}
onClick={() =>
setSavedAddresses([
...savedAddresses,
{
address: newAddressInput,
label: newLableInput,
},
])
}
>
<HStack>
<FontAwesomeIcon icon={faSave} />
<Text>Save</Text>
</HStack>
</Button>
</Center>
{savedAddresses.length > 0 && (
<Box mt="6" px="20">
<Text fontWeight={"bold"}>Select from saved addresses:</Text>
<Box mt="3" px="10">
{savedAddresses.map(({ address, label }, i) => (
<HStack key={i} mt="2">
<Button
key={i}
w="100%"
onClick={() => {
setShowAddress(address);
setAddress(address);
closeAddressBook();
}}
>
{label} (
{address.indexOf(".eth") >= 0
? address
: slicedText(address)}
)
</Button>
<Button
ml="2"
_hover={{
bg: "red.500",
}}
onClick={() => {
const _savedAddresses = savedAddresses;
_savedAddresses.splice(i, 1);
// using spread operator, else useEffect doesn't detect state change
setSavedAddresses([..._savedAddresses]);
}}
>
<DeleteIcon />
</Button>
</HStack>
))}
</Box>
</Box>
)}
</ModalBody>
</ModalContent>
</Modal>
);
}
export default AddressBook;

View File

@@ -0,0 +1,104 @@
import {
FormControl,
FormLabel,
InputGroup,
Input,
InputRightElement,
Button,
HStack,
useDisclosure,
} from "@chakra-ui/react";
import { DeleteIcon } from "@chakra-ui/icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBook } from "@fortawesome/free-solid-svg-icons";
import AddressBook from "./AddressBook";
interface AddressInputParams {
showAddress: string;
setShowAddress: (value: string) => void;
setAddress: (value: string) => void;
setIsAddressValid: (value: boolean) => void;
isAddressValid: boolean;
selectedTabIndex: number;
isConnected: boolean;
appUrl: string | undefined;
isIFrameLoading: boolean;
updateAddress: () => void;
}
function AddressInput({
showAddress,
setShowAddress,
setAddress,
setIsAddressValid,
isAddressValid,
selectedTabIndex,
isConnected,
appUrl,
isIFrameLoading,
updateAddress,
}: AddressInputParams) {
const {
isOpen: isAddressBookOpen,
onOpen: openAddressBook,
onClose: closeAddressBook,
} = useDisclosure();
return (
<FormControl>
<FormLabel>Enter Address or ENS to Impersonate</FormLabel>
<HStack>
<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={"brand.lightBlack"}
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>
) : (
showAddress && (
<InputRightElement px="1rem" mr="0.5rem">
<Button
h="1.75rem"
size="sm"
onClick={() => {
setShowAddress("");
setAddress("");
}}
>
<DeleteIcon />
</Button>
</InputRightElement>
)
)}
</InputGroup>
<Button onClick={openAddressBook}>
<FontAwesomeIcon icon={faBook} />
</Button>
<AddressBook
isAddressBookOpen={isAddressBookOpen}
closeAddressBook={closeAddressBook}
showAddress={showAddress}
setShowAddress={setShowAddress}
setAddress={setAddress}
/>
</HStack>
</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,25 @@
import { Button, useToast } from "@chakra-ui/react";
import { CopyIcon } from "@chakra-ui/icons";
const CopyToClipboard = ({ txt }: { txt: string }) => {
const toast = useToast();
return (
<Button
onClick={() => {
navigator.clipboard.writeText(txt);
toast({
title: "Copied to clipboard",
status: "success",
isClosable: true,
duration: 1000,
});
}}
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,59 @@
import {
HStack,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Text,
Input,
} from "@chakra-ui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faShareAlt } from "@fortawesome/free-solid-svg-icons";
import CopyToClipboard from "../CopyToClipboard";
interface ShareModalParams {
isOpen: boolean;
onClose: () => void;
appUrl: string;
showAddress: string;
}
function ShareModal({
isOpen,
onClose,
appUrl,
showAddress,
}: ShareModalParams) {
const urlToShare = `https://impersonator.xyz/?address=${showAddress}&url=${encodeURI(
appUrl
)}`;
return (
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay bg="none" backdropFilter="auto" backdropBlur="5px" />
<ModalContent>
<ModalHeader>
<HStack>
<FontAwesomeIcon icon={faShareAlt} />
<Text>Share</Text>
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text>
Share this link so that anyone can auto-connect to this dapp with
your provided address!
</Text>
<HStack my="3">
<Input value={urlToShare} isReadOnly />
<CopyToClipboard txt={urlToShare} />
</HStack>
</ModalBody>
</ModalContent>
</Modal>
);
}
export default ShareModal;

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 bg="white" 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,147 @@
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.safe.global/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",
}}
bg={"brand.lightBlack"}
>
<ModalHeader>Select a dapp</ModalHeader>
<ModalCloseButton />
<ModalBody>
{(!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,140 @@
import {
Button,
Box,
Center,
Spacer,
HStack,
FormControl,
Input,
Text,
useDisclosure,
InputGroup,
InputRightElement,
} from "@chakra-ui/react";
import { DeleteIcon } from "@chakra-ui/icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faShareAlt } from "@fortawesome/free-solid-svg-icons";
import SupportedDapps from "./SupportedDapps";
import AppUrlLabel from "./AppUrlLabel";
import ShareModal from "./ShareModal";
interface IFrameConnectTabParams {
networkId: number;
initIFrame: (_inputAppUrl?: string | undefined) => Promise<void>;
inputAppUrl: string | undefined;
setInputAppUrl: (value: string | undefined) => void;
appUrl: string | undefined;
isIFrameLoading: boolean;
setIsIFrameLoading: (value: boolean) => void;
iframeKey: number;
iframeRef: React.RefObject<HTMLIFrameElement> | null;
showAddress: string;
}
function IFrameConnectTab({
networkId,
initIFrame,
setInputAppUrl,
inputAppUrl,
isIFrameLoading,
appUrl,
iframeKey,
iframeRef,
setIsIFrameLoading,
showAddress,
}: IFrameConnectTabParams) {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<FormControl my={4}>
<HStack>
<AppUrlLabel />
<Spacer />
<SupportedDapps
networkId={networkId}
initIFrame={initIFrame}
setInputAppUrl={setInputAppUrl}
/>
</HStack>
<HStack mt="2">
<InputGroup>
<Input
pr="3.5rem"
bg={"brand.lightBlack"}
placeholder="https://app.uniswap.org/"
aria-label="dapp-url"
autoComplete="off"
value={inputAppUrl}
onChange={(e) => setInputAppUrl(e.target.value)}
/>
{inputAppUrl && (
<InputRightElement px="1rem" mr="0.5rem">
<Button
h="1.75rem"
size="sm"
onClick={() => {
setInputAppUrl("");
}}
>
<DeleteIcon />
</Button>
</InputRightElement>
)}
</InputGroup>
{appUrl && (
<>
<Button onClick={onOpen}>
<HStack>
<FontAwesomeIcon icon={faShareAlt} />
<Text>Share</Text>
</HStack>
</Button>
<ShareModal
isOpen={isOpen}
onClose={onClose}
appUrl={appUrl}
showAddress={showAddress}
/>
</>
)}
</HStack>
</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,72 @@
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={{
menuList: (provided) => ({
...provided,
bg: "brand.black",
}),
option: (provided, state) => ({
...provided,
color: "white",
bg: state.isFocused ? "whiteAlpha500" : "brand.lightBlack",
}),
groupHeading: (provided, state) => ({
...provided,
h: "1px",
borderTop: "1px solid white",
bg: "brand.black",
}),
}}
closeMenuOnSelect
useBasicStyles
/>
</Box>
);
}
export default NetworkInput;

View File

@@ -0,0 +1,63 @@
import { useState, useEffect } from "react";
import {
Alert,
AlertIcon,
CloseButton,
Text,
Link,
HStack,
Center,
} from "@chakra-ui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import { ExternalLinkIcon } from "@chakra-ui/icons";
import axios from "axios";
const CLOSED_KEY = "new-ui-notif-closed";
function NotificationBar() {
const isClosed = localStorage.getItem(CLOSED_KEY);
const [isVisible, setIsVisible] = useState(
isClosed === "true" ? false : true
);
useEffect(() => {
if (!isVisible) {
localStorage.setItem(CLOSED_KEY, "true");
}
}, [isVisible]);
// const [donor, setDonor] = useState<string>();
// useEffect(() => {
// axios
// .get("https://api.impersonator.xyz/api")
// .then((res) => {
// setDonor(res.data);
// })
// .catch((err) => {
// console.log(err);
// });
// }, []);
return isVisible ? (
<Alert status="info" bg={"#151515"}>
<Center w="100%">
<Text>
<span style={{ fontSize: "1.2rem" }}></span>{" "}
<span style={{ fontWeight: "bold" }}>New UI is here</span>
</Text>
</Center>
<CloseButton
alignSelf="flex-start"
position="relative"
right={-1}
top={-1}
onClick={() => setIsVisible(false)}
/>
</Alert>
) : null;
}
export default NotificationBar;

48
components/Body/Tab.tsx Normal file
View File

@@ -0,0 +1,48 @@
import { Box, HStack, Badge } from "@chakra-ui/react";
const Tab = ({
children,
tabIndex,
selectedTabIndex,
setSelectedTabIndex,
isNew = false,
}: {
children: React.ReactNode;
tabIndex: number;
selectedTabIndex: number;
setSelectedTabIndex: Function;
isNew?: boolean;
}) => {
return (
<HStack
fontWeight={"semibold"}
color={tabIndex === selectedTabIndex ? "white" : "whiteAlpha.700"}
role="group"
_hover={{
color: "whiteAlpha.900",
}}
cursor="pointer"
onClick={() => setSelectedTabIndex(tabIndex)}
>
<Box>{children}</Box>
{isNew && (
<Box pb="5">
<Badge
variant="subtle"
_groupHover={{
bg: "green.500",
color: "whiteAlpha.800",
}}
colorScheme="green"
rounded={"md"}
fontSize={"10"}
>
New
</Badge>
</Box>
)}
</HStack>
);
};
export default Tab;

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"}
bg={"brand.lightBlack"}
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,90 @@
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}
bg="brand.lightBlack"
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,175 @@
import {
Box,
Flex,
HStack,
Text,
Heading,
Tooltip,
Td,
Collapse,
useDisclosure,
Button,
Table,
Thead,
Tr,
Th,
Tbody,
Link,
} from "@chakra-ui/react";
import {
InfoIcon,
ChevronDownIcon,
ChevronUpIcon,
DeleteIcon,
UnlockIcon,
} from "@chakra-ui/icons";
import CopyToClipboard from "./CopyToClipboard";
import { TxnDataType } from "../../types";
import { useEffect } from "react";
import { ethers } from "ethers";
export 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>
);
const ValueTD = ({ txt }: { txt: string }) => (
<Td>
<HStack>
<Tooltip label={`${txt} Wei`} hasArrow placement="top">
<Text>{ethers.utils.formatEther(txt)} ETH</Text>
</Tooltip>
<CopyToClipboard txt={txt} />
</HStack>
</Td>
);
const TData = ({
calldata,
address,
networkId,
}: {
calldata: string;
address: string;
networkId: number;
}) => (
<Td>
<HStack>
<Tooltip label={calldata} hasArrow placement="top">
<Text>{slicedText(calldata)}</Text>
</Tooltip>
<CopyToClipboard txt={calldata} />
<Button title="Decode" size="sm">
<Link
href={`https://calldata.swiss-knife.xyz/decoder?calldata=${calldata}&address=${address}&chainId=${networkId}`}
isExternal
>
<UnlockIcon />
</Link>
</Button>
</HStack>
</Td>
);
interface TransactionRequestsParams {
sendTxnData: TxnDataType[];
setSendTxnData: (value: TxnDataType[]) => void;
networkId: number;
}
function TransactionRequests({
sendTxnData,
setSendTxnData,
networkId,
}: TransactionRequestsParams) {
const {
isOpen: tableIsOpen,
onOpen: tableOnOpen,
onToggle: tableOnToggle,
} = useDisclosure();
useEffect(() => {
// keep table open on load
tableOnOpen();
}, []);
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} />
<TData calldata={d.data} address={d.to} networkId={networkId} />
<ValueTD 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,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,90 @@
import { useState, useEffect } from "react";
import {
FormControl,
HStack,
FormLabel,
Tooltip,
Box,
Text,
Input,
InputGroup,
InputRightElement,
Button,
} from "@chakra-ui/react";
import { InfoIcon, DeleteIcon } from "@chakra-ui/icons";
interface URIInputParams {
uri: string;
setUri: (value: string) => void;
isConnected: boolean;
initWalletConnect: () => void;
}
function URIInput({
uri,
setUri,
isConnected,
initWalletConnect,
}: URIInputParams) {
const [pasted, setPasted] = useState(false);
useEffect(() => {
if (pasted) {
initWalletConnect();
setPasted(false);
}
}, [uri]);
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>
<InputGroup>
<Input
pr={isConnected ? "0" : "3.5rem"}
bg={"brand.lightBlack"}
placeholder="wc:xyz123"
aria-label="uri"
autoComplete="off"
value={uri}
onChange={(e) => setUri(e.target.value)}
onPaste={(e) => {
e.preventDefault();
setPasted(true);
setUri(e.clipboardData.getData("text"));
}}
isDisabled={isConnected}
/>
{uri && !isConnected && (
<InputRightElement px="1rem" mr="0.5rem">
<Button h="1.75rem" size="sm" onClick={() => setUri("")}>
<DeleteIcon />
</Button>
</InputRightElement>
)}
</InputGroup>
</Box>
</FormControl>
);
}
export default URIInput;

View File

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

701
components/Body/index.tsx Normal file
View File

@@ -0,0 +1,701 @@
"use client";
import { useState, useEffect } from "react";
import { Container, useToast, Center, Spacer, Flex } from "@chakra-ui/react";
import { SingleValue } from "chakra-react-select";
// WC v2
import { Core } from "@walletconnect/core";
import { Web3Wallet, IWeb3Wallet } from "@walletconnect/web3wallet";
import { ProposalTypes, SessionTypes } from "@walletconnect/types";
import { getSdkError, parseUri } from "@walletconnect/utils";
import { ethers } from "ethers";
import axios from "axios";
import networksList from "evm-rpcs-list";
import { useSafeInject } from "../../contexts/SafeInjectContext";
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";
import NotificationBar from "./NotificationBar";
const WCMetadata = {
name: "Impersonator",
description: "Login to dapps as any address",
url: "www.impersonator.xyz",
icons: ["https://www.impersonator.xyz/favicon.ico"],
};
const core = new Core({
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID,
});
const primaryNetworkIds = [
1, // ETH Mainnet
42161, // Arbitrum One
43114, // Avalanche
56, // BSC
250, // Fantom Opera
5, // Goerli Testnet
100, // Gnosis
10, // Optimism
137, // Polygon
];
const primaryNetworkOptions = primaryNetworkIds.map((id) => {
return { chainId: id, ...networksList[id.toString()] };
});
const secondaryNetworkOptions = Object.entries(networksList)
.filter((id) => !primaryNetworkIds.includes(parseInt(id[0])))
.map((arr) => {
return {
chainId: parseInt(arr[0]),
name: arr[1].name,
rpcs: arr[1].rpcs,
};
});
const allNetworksOptions = [
...primaryNetworkOptions,
...secondaryNetworkOptions,
];
function Body() {
const addressFromURL = new URLSearchParams(window.location.search).get(
"address"
);
const urlFromURL = new URLSearchParams(window.location.search).get("url");
const urlFromCache = localStorage.getItem("appUrl");
const chainFromURL = new URLSearchParams(window.location.search).get("chain");
let networkIdViaURL = 1;
if (chainFromURL) {
for (let i = 0; i < allNetworksOptions.length; i++) {
if (
allNetworksOptions[i].name
.toLowerCase()
.includes(chainFromURL.toLowerCase())
) {
networkIdViaURL = allNetworksOptions[i].chainId;
break;
}
}
}
const toast = useToast();
const {
setAddress: setIFrameAddress,
appUrl,
setAppUrl,
setRpcUrl,
iframeRef,
latestTransaction,
} = useSafeInject();
const [provider, setProvider] = useState<ethers.providers.JsonRpcProvider>();
const [showAddress, setShowAddress] = useState(addressFromURL ?? ""); // gets displayed in input. ENS name remains as it is
const [address, setAddress] = useState(addressFromURL ?? ""); // internal resolved address
const [isAddressValid, setIsAddressValid] = useState(true);
const [uri, setUri] = useState("");
const [networkId, setNetworkId] = useState(networkIdViaURL);
const [selectedNetworkOption, setSelectedNetworkOption] = useState<
SingleValue<SelectedNetworkOption>
>({
label: networksList[networkIdViaURL].name,
value: networkIdViaURL,
});
// WC v2
const [web3wallet, setWeb3Wallet] = useState<IWeb3Wallet>();
const [web3WalletSession, setWeb3WalletSession] =
useState<SessionTypes.Struct>();
const [isConnected, setIsConnected] = useState(false);
const [loading, setLoading] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(urlFromURL ? 1 : 0);
const [isIFrameLoading, setIsIFrameLoading] = useState(false);
const [inputAppUrl, setInputAppUrl] = useState<string | undefined>(
urlFromURL ?? urlFromCache ?? undefined
);
const [iframeKey, setIframeKey] = useState(0); // hacky way to reload iframe when key changes
const [tenderlyForkId, setTenderlyForkId] = useState("");
const [sendTxnData, setSendTxnData] = useState<TxnDataType[]>([]);
useEffect(() => {
// only use cached address if no address from url provided
if (!addressFromURL) {
// getCachedSession
const _showAddress = localStorage.getItem("showAddress") ?? undefined;
// WC V2
initWeb3Wallet(true, _showAddress);
}
setProvider(
new ethers.providers.JsonRpcProvider(
`https://mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_KEY}`
)
);
const storedTenderlyForkId = localStorage.getItem("tenderlyForkId");
setTenderlyForkId(storedTenderlyForkId ? storedTenderlyForkId : "");
}, []);
useEffect(() => {
updateNetwork((selectedNetworkOption as SelectedNetworkOption).value);
// eslint-disable-next-line
}, [selectedNetworkOption]);
useEffect(() => {
if (provider && addressFromURL && urlFromURL) {
initIFrame();
}
// eslint-disable-next-line
}, [provider]);
useEffect(() => {
if (web3wallet) {
subscribeToEvents();
}
// eslint-disable-next-line
}, [web3wallet]);
useEffect(() => {
localStorage.setItem("tenderlyForkId", tenderlyForkId);
}, [tenderlyForkId]);
useEffect(() => {
localStorage.setItem("showAddress", showAddress);
}, [showAddress]);
useEffect(() => {
if (inputAppUrl) {
localStorage.setItem("appUrl", inputAppUrl);
}
}, [inputAppUrl]);
useEffect(() => {
setIFrameAddress(address);
// eslint-disable-next-line
}, [address]);
useEffect(() => {
// TODO: use random rpc if this one is slow/down?
setRpcUrl(networksList[networkId].rpcs[0]);
// eslint-disable-next-line
}, [networkId]);
useEffect(() => {
if (latestTransaction) {
const newTxn = {
from: address,
...latestTransaction,
};
setSendTxnData((data) => {
if (data.some((d) => d.id === newTxn.id)) {
return data;
} else {
return [
{ ...newTxn, value: parseInt(newTxn.value, 16).toString() },
...data,
];
}
});
if (tenderlyForkId.length > 0) {
axios
.post("https://rpc.tenderly.co/fork/" + tenderlyForkId, {
jsonrpc: "2.0",
id: newTxn.id,
method: "eth_sendTransaction",
params: [
{
from: newTxn.from,
to: newTxn.to,
value: newTxn.value,
data: newTxn.data,
},
],
})
.then((res) => console.log(res.data));
}
}
// eslint-disable-next-line
}, [latestTransaction, tenderlyForkId]);
const initWeb3Wallet = async (
onlyIfActiveSessions?: boolean,
_showAddress?: string
) => {
const _web3wallet = await Web3Wallet.init({
core,
metadata: WCMetadata,
});
if (onlyIfActiveSessions) {
const sessions = _web3wallet.getActiveSessions();
const sessionsArray = Object.values(sessions);
if (sessionsArray.length > 0) {
const _address =
sessionsArray[0].namespaces["eip155"].accounts[0].split(":")[2];
console.log({ _showAddress, _address });
setWeb3WalletSession(sessionsArray[0]);
setShowAddress(
_showAddress && _showAddress.length > 0 ? _showAddress : _address
);
if (!(_showAddress && _showAddress.length > 0)) {
localStorage.setItem("showAddress", _address);
}
setAddress(_address);
setUri(
`wc:${sessionsArray[0].pairingTopic}@2?relay-protocol=irn&symKey=xxxxxx`
);
setWeb3Wallet(_web3wallet);
setIsConnected(true);
}
} else {
setWeb3Wallet(_web3wallet);
if (_showAddress) {
setShowAddress(_showAddress);
setAddress(_showAddress);
}
}
// for debugging
(window as any).w3 = _web3wallet;
};
const resolveAndValidateAddress = async () => {
let isValid;
let _address = address;
if (!address) {
isValid = false;
} else {
// Resolve ENS
const resolvedAddress = await provider!.resolveName(address);
if (resolvedAddress) {
setAddress(resolvedAddress);
_address = resolvedAddress;
isValid = true;
} else if (ethers.utils.isAddress(address)) {
isValid = true;
} else {
isValid = false;
}
}
setIsAddressValid(isValid);
if (!isValid) {
toast({
title: "Invalid Address",
description: "Address is not an ENS or Ethereum address",
status: "error",
isClosable: true,
duration: 4000,
});
}
return { isValid, _address: _address };
};
const initWalletConnect = async () => {
setLoading(true);
const { isValid } = await resolveAndValidateAddress();
if (isValid) {
const { version } = parseUri(uri);
try {
if (version === 1) {
toast({
title: "Couldn't Connect",
description:
"The dapp is still using the deprecated WalletConnect V1",
status: "error",
isClosable: true,
duration: 8000,
});
setLoading(false);
// let _legacySignClient = new LegacySignClient({ uri });
// if (!_legacySignClient.connected) {
// await _legacySignClient.createSession();
// }
// setLegacySignClient(_legacySignClient);
// setUri(_legacySignClient.uri);
} else {
await initWeb3Wallet();
}
} catch (err) {
console.error(err);
toast({
title: "Couldn't Connect",
description: "Refresh dApp and Connect again",
status: "error",
isClosable: true,
duration: 2000,
});
setLoading(false);
}
} else {
setLoading(false);
}
};
const initIFrame = async (_inputAppUrl = inputAppUrl) => {
setIsIFrameLoading(true);
if (_inputAppUrl === appUrl) {
setIsIFrameLoading(false);
return;
}
const { isValid } = await resolveAndValidateAddress();
if (!isValid) {
setIsIFrameLoading(false);
return;
}
setAppUrl(_inputAppUrl);
};
const subscribeToEvents = async () => {
console.log("ACTION", "subscribeToEvents");
if (web3wallet) {
web3wallet.on("session_proposal", async (proposal) => {
if (loading) {
setLoading(false);
}
console.log("EVENT", "session_proposal", proposal);
const { requiredNamespaces, optionalNamespaces } = proposal.params;
const namespaceKey = "eip155";
const requiredNamespace = requiredNamespaces[namespaceKey] as
| ProposalTypes.BaseRequiredNamespace
| undefined;
const optionalNamespace = optionalNamespaces
? optionalNamespaces[namespaceKey]
: undefined;
let chains: string[] | undefined =
requiredNamespace === undefined
? undefined
: requiredNamespace.chains;
if (optionalNamespace && optionalNamespace.chains) {
if (chains) {
// merge chains from requiredNamespace & optionalNamespace, while avoiding duplicates
chains = Array.from(
new Set(chains.concat(optionalNamespace.chains))
);
} else {
chains = optionalNamespace.chains;
}
}
const accounts: string[] = [];
chains?.map((chain) => {
accounts.push(`${chain}:${address}`);
return null;
});
const namespace: SessionTypes.Namespace = {
accounts,
chains: chains,
methods:
requiredNamespace === undefined ? [] : requiredNamespace.methods,
events:
requiredNamespace === undefined ? [] : requiredNamespace.events,
};
if (requiredNamespace && requiredNamespace.chains) {
const _chainId = parseInt(requiredNamespace.chains[0].split(":")[1]);
setSelectedNetworkOption({
label: networksList[_chainId].name,
value: _chainId,
});
}
const session = await web3wallet.approveSession({
id: proposal.id,
namespaces: {
[namespaceKey]: namespace,
},
});
setWeb3WalletSession(session);
setIsConnected(true);
});
try {
await web3wallet.core.pairing.pair({ uri });
} catch (e) {
console.error(e);
}
web3wallet.on("session_request", async (event) => {
const { topic, params, id } = event;
const { request } = params;
console.log("EVENT", "session_request", event);
if (request.method === "eth_sendTransaction") {
await handleSendTransaction(id, request.params, topic);
} else {
await web3wallet.respondSessionRequest({
topic,
response: {
jsonrpc: "2.0",
id: id,
error: {
code: 0,
message: "Method not supported by Impersonator",
},
},
});
}
});
web3wallet.on("session_delete", () => {
console.log("EVENT", "session_delete");
reset();
});
}
};
const handleSendTransaction = async (
id: number,
params: any[],
topic?: string
) => {
setSendTxnData((data) => {
const newTxn = {
id: id,
from: params[0].from,
to: params[0].to,
data: params[0].data,
value: params[0].value ? parseInt(params[0].value, 16).toString() : "0",
};
if (data.some((d) => d.id === newTxn.id)) {
return data;
} else {
return [newTxn, ...data];
}
});
if (tenderlyForkId.length > 0) {
const { data: res } = await axios.post(
"https://rpc.tenderly.co/fork/" + tenderlyForkId,
{
jsonrpc: "2.0",
id: id,
method: "eth_sendTransaction",
params: params,
}
);
console.log({ res });
// Approve Call Request
if (web3wallet && topic) {
await web3wallet.respondSessionRequest({
topic,
response: {
jsonrpc: "2.0",
id: res.id,
result: res.result,
},
});
}
toast({
title: "Txn successful",
description: `Hash: ${res.result}`,
status: "success",
position: "bottom-right",
duration: null,
isClosable: true,
});
} else {
if (web3wallet && topic) {
await web3wallet.respondSessionRequest({
topic,
response: {
jsonrpc: "2.0",
id: id,
error: { code: 0, message: "Method not supported by Impersonator" },
},
});
}
}
};
const updateSession = async ({
newChainId,
newAddress,
}: {
newChainId?: number;
newAddress?: string;
}) => {
let _chainId = newChainId || networkId;
let _address = newAddress || address;
if (web3wallet && web3WalletSession) {
await web3wallet.emitSessionEvent({
topic: web3WalletSession.topic,
event: {
name: _chainId !== networkId ? "chainChanged" : "accountsChanged",
data: [_address],
},
chainId: `eip155:${_chainId}`,
});
setLoading(false);
} else {
setLoading(false);
}
};
const updateAddress = async () => {
if (selectedTabIndex === 0) {
setLoading(true);
} else {
setIsIFrameLoading(true);
}
const { isValid, _address } = await resolveAndValidateAddress();
if (isValid) {
if (selectedTabIndex === 0) {
updateSession({
newAddress: _address,
});
} else {
setIFrameAddress(_address);
setIframeKey((key) => key + 1);
setIsIFrameLoading(false);
}
}
};
const updateNetwork = (_networkId: number) => {
setNetworkId(_networkId);
if (selectedTabIndex === 0) {
updateSession({
newChainId: _networkId,
});
} else {
setIframeKey((key) => key + 1);
}
};
const killSession = async () => {
console.log("ACTION", "killSession");
if (web3wallet && web3WalletSession) {
try {
await web3wallet.disconnectSession({
topic: web3WalletSession.topic,
reason: getSdkError("USER_DISCONNECTED"),
});
} catch (e) {
console.error("killSession", e);
}
setWeb3WalletSession(undefined);
setUri("");
setIsConnected(false);
}
};
const reset = (persistUri?: boolean) => {
setWeb3WalletSession(undefined);
setIsConnected(false);
if (!persistUri) {
setUri("");
}
localStorage.removeItem("walletconnect");
};
return (
<>
<NotificationBar />
<Container mt="10" mb="16" minW={["0", "0", "2xl", "2xl"]}>
<Flex>
<Spacer flex="1" />
<TenderlySettings
tenderlyForkId={tenderlyForkId}
setTenderlyForkId={setTenderlyForkId}
/>
</Flex>
<AddressInput
showAddress={showAddress}
setShowAddress={setShowAddress}
setAddress={setAddress}
setIsAddressValid={setIsAddressValid}
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 (
<WalletConnectTab
uri={uri}
setUri={setUri}
isConnected={isConnected}
initWalletConnect={initWalletConnect}
loading={loading}
setLoading={setLoading}
reset={reset}
killSession={killSession}
web3WalletSession={web3WalletSession}
/>
);
case 1:
return (
<IFrameConnectTab
networkId={networkId}
initIFrame={initIFrame}
setInputAppUrl={setInputAppUrl}
inputAppUrl={inputAppUrl}
isIFrameLoading={isIFrameLoading}
appUrl={appUrl}
iframeKey={iframeKey}
iframeRef={iframeRef}
setIsIFrameLoading={setIsIFrameLoading}
showAddress={showAddress}
/>
);
case 2:
return <BrowserExtensionTab />;
}
})()}
<Center>
<TransactionRequests
sendTxnData={sendTxnData}
setSendTxnData={setSendTxnData}
networkId={networkId}
/>
</Center>
</Container>
</>
);
}
export default Body;

View File

@@ -0,0 +1,123 @@
import { Box, Button, Image, Text } from "@chakra-ui/react";
import { ChevronDownIcon } from "@chakra-ui/icons";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { blo } from "blo";
export const CustomConnectButton = () => {
return (
<ConnectButton.Custom>
{({
account,
chain,
openAccountModal,
openChainModal,
openConnectModal,
authenticationStatus,
mounted,
}) => {
// Note: If your app doesn't use authentication, you
// can remove all 'authenticationStatus' checks
const ready: boolean = mounted && authenticationStatus !== "loading";
const connected =
ready &&
account &&
chain &&
(!authenticationStatus || authenticationStatus === "authenticated");
return ready ? (
<Box hidden={!ready}>
{(() => {
if (!connected) {
return (
<Button colorScheme={"telegram"} onClick={openConnectModal}>
Connect Wallet
</Button>
);
}
if (chain.unsupported) {
return (
<Button colorScheme={"red"} onClick={openChainModal}>
Wrong network
</Button>
);
}
return (
<Box
display="flex"
py="0"
alignItems="center"
borderRadius="xl"
>
<Button
mr={2}
pr={2}
bg={"gray.800"}
_hover={{
bg: "whiteAlpha.200",
}}
onClick={openChainModal}
borderRadius="xl"
>
{chain.hasIcon && (
<Box
mr={4}
w={6}
bgImg={chain.iconBackground}
overflow={"hidden"}
rounded={"full"}
>
{chain.iconUrl && (
<Image
w={6}
alt={chain.name ?? "Chain icon"}
src={chain.iconUrl}
/>
)}
</Box>
)}
{chain.name}
<ChevronDownIcon ml={1} pt={1} fontSize="2xl" />
</Button>
<Button
onClick={openAccountModal}
bg="blackAlpha.500"
border="1px solid transparent"
_hover={{
border: "1px",
borderStyle: "solid",
borderColor: "blue.400",
backgroundColor: "gray.700",
}}
borderRadius="xl"
m="1px"
px={3}
h="38px"
>
<Text
color="white"
fontSize="md"
fontWeight="medium"
mr="2"
>
{account.displayName}
</Text>
<Image
src={
account.ensAvatar ??
blo(account.address as `0x${string}`)
}
w="24px"
h="24px"
rounded={"full"}
alt={account.displayName}
/>
</Button>
</Box>
);
})()}
</Box>
) : null;
}}
</ConnectButton.Custom>
);
};

227
components/Footer.tsx Normal file
View File

@@ -0,0 +1,227 @@
"use client";
import { useState } from "react";
import {
Flex,
VStack,
Heading,
Spacer,
Link,
Text,
Alert,
HStack,
Stack,
Center,
Button,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
SimpleGrid,
GridItem,
Input,
InputGroup,
Container,
InputRightElement,
Box,
} from "@chakra-ui/react";
import { ExternalLinkIcon } from "@chakra-ui/icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faTwitter, faDiscord } from "@fortawesome/free-brands-svg-icons";
import { useAccount, useNetwork } from "wagmi";
import { sendTransaction } from "wagmi/actions";
import { parseEther } from "viem";
import Confetti from "react-confetti";
import { CustomConnectButton } from "./CustomConnectButton";
const Social = ({ icon, link }: { icon: IconProp; link: string }) => {
return (
<Link href={link} isExternal>
<FontAwesomeIcon icon={icon} size="lg" />
</Link>
);
};
function Footer() {
const { isConnected } = useAccount();
const { chain } = useNetwork();
const {
isOpen: isSupportModalOpen,
onOpen: openSupportModal,
onClose: closeSupportModal,
} = useDisclosure();
const [donateValue, setDonateValue] = useState<string>();
const [showConfetti, setShowConfetti] = useState(false);
const handleDonate = async (value: string) => {
try {
await sendTransaction({
to: process.env.NEXT_PUBLIC_DONATION_ADDRESS!,
value: parseEther(value),
});
launchConfetti();
} catch {}
};
const launchConfetti = () => {
setShowConfetti(true);
setTimeout(() => {
setShowConfetti(false);
}, 5_000);
};
return (
<Flex py="4" borderTop="2px" borderTopColor={"gray.400"}>
<Spacer flex="1" />
{showConfetti && (
<Box zIndex={9999} position={"fixed"} top={0} left={0}>
<Confetti
recycle={false}
gravity={0.15}
numberOfPieces={2_000}
wind={0.005}
/>
</Box>
)}
<VStack>
<Alert
status="info"
bg={"whiteAlpha.200"}
color="white"
variant="solid"
rounded="lg"
>
<Stack direction={{ base: "column", md: "row" }}>
<Center>Found the project helpful?</Center>
<HStack>
{process.env.NEXT_PUBLIC_GITCOIN_GRANTS_ACTIVE === "true" ? (
<>
<Text>Support it on</Text>
<Link
href={process.env.NEXT_PUBLIC_GITCOIN_GRANTS_LINK}
isExternal
>
<HStack fontWeight="bold" textDecor="underline">
<Text>Gitcoin Grants</Text>
<ExternalLinkIcon />
</HStack>
</Link>
</>
) : (
<>
<Button
size={"sm"}
fontWeight={"bold"}
onClick={() => {
openSupportModal();
}}
bg={"blackAlpha.500"}
>
Support!
</Button>
<Modal
isOpen={isSupportModalOpen}
onClose={closeSupportModal}
isCentered
>
<ModalOverlay
bg="none"
backdropFilter="auto"
backdropBlur="3px"
/>
<ModalContent bg={"brand.lightBlack"}>
<ModalHeader>Support</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<Container>
<Center>
<CustomConnectButton />
</Center>
<Text mt={4} size="md">
Select amount to donate:
</Text>
<SimpleGrid mt={3} columns={3}>
{["0.001", "0.005", "0.01"].map((value, i) => (
<GridItem key={i}>
<Center>
<Button
onClick={() => handleDonate(value)}
isDisabled={
!isConnected || chain?.unsupported
}
>
{value} Ξ
</Button>
</Center>
</GridItem>
))}
</SimpleGrid>
<Center mt={4}>or</Center>
<InputGroup mt={4}>
<Input
type="number"
placeholder="Custom amount"
onChange={(e) => setDonateValue(e.target.value)}
isDisabled={!isConnected || chain?.unsupported}
/>
<InputRightElement
bg="gray.600"
fontWeight={"bold"}
roundedRight={"lg"}
>
Ξ
</InputRightElement>
</InputGroup>
<Center mt={2}>
<Button
onClick={() => {
if (donateValue) {
handleDonate(donateValue);
}
}}
isDisabled={!donateValue || chain?.unsupported}
>
Donate
</Button>
</Center>
</Container>
</ModalBody>
</ModalContent>
</Modal>
</>
)}
</HStack>
</Stack>
</Alert>
<Heading size="md">
Built by:{" "}
<Social icon={faTwitter} link="https://twitter.com/apoorvlathey" />
<Link href="https://twitter.com/apoorvlathey" isExternal>
<Text decoration="underline" display="inline">
@apoorvlathey
</Text>{" "}
<ExternalLinkIcon />
</Link>
</Heading>
<Center pt="1">
<Link
href={"https://discord.gg/4VTnuVzfmm"}
color="twitter.200"
isExternal
>
<FontAwesomeIcon icon={faDiscord} size="2x" />
</Link>
</Center>
</VStack>
<Spacer flex="1" />
</Flex>
);
}
export default Footer;

48
components/Navbar.tsx Normal file
View File

@@ -0,0 +1,48 @@
import {
Button,
Flex,
Heading,
Spacer,
Box,
Link,
HStack,
Text,
Image,
} from "@chakra-ui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGithub } from "@fortawesome/free-brands-svg-icons";
function Navbar() {
return (
<Flex
py="4"
px={["2", "4", "10", "10"]}
borderBottom="2px"
borderBottomColor={"gray.400"}
>
<Spacer flex="1" />
<Heading
maxW={["302px", "4xl", "4xl", "4xl"]}
fontSize={{ base: "2xl", sm: "3xl", md: "4xl" }}
pr="2rem"
>
<HStack>
<Image src="/logo-no-bg.png" w="3rem" mr="1rem" />
<Text>Impersonator</Text>
</HStack>
</Heading>
<Flex flex="1" justifyContent="flex-end" alignItems={"center"}>
<Box pl="1rem">
<Link
href={"https://github.com/apoorvlathey/impersonator"}
isExternal
>
<FontAwesomeIcon icon={faGithub} size="2x" />
</Link>
</Box>
</Flex>
</Flex>
);
}
export default Navbar;

View File

@@ -0,0 +1,30 @@
import { Providers } from "@/app/providers";
import { Analytics } from "@/components/Analytics";
export const IndexLayout = ({ children }: { children: React.ReactNode }) => {
return (
<html lang="en">
{/* Farcaster Frame */}
<head>
<meta property="fc:frame" content="vNext" />
<meta
property="fc:frame:image"
content="https://frame.impersonator.xyz/impersonator.gif"
/>
<meta
property="fc:frame:post_url"
content="https://frame.impersonator.xyz/api/frame"
/>
<meta
property="fc:frame:input:text"
content="Type ENS or Address to impersonate..."
/>
<meta property="fc:frame:button:1" content="🕵️ Start" />
</head>
<body>
<Analytics />
<Providers>{children}</Providers>
</body>
</html>
);
};