feat: move to nextjs
This commit is contained in:
26
components/Analytics.tsx
Normal file
26
components/Analytics.tsx
Normal 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}');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
161
components/Body/AddressInput/AddressBook/index.tsx
Normal file
161
components/Body/AddressInput/AddressBook/index.tsx
Normal 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;
|
||||
104
components/Body/AddressInput/index.tsx
Normal file
104
components/Body/AddressInput/index.tsx
Normal 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;
|
||||
43
components/Body/BrowserExtensionTab.tsx
Normal file
43
components/Body/BrowserExtensionTab.tsx
Normal 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;
|
||||
25
components/Body/CopyToClipboard.tsx
Normal file
25
components/Body/CopyToClipboard.tsx
Normal 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;
|
||||
29
components/Body/IFrameConnectTab/AppUrlLabel.tsx
Normal file
29
components/Body/IFrameConnectTab/AppUrlLabel.tsx
Normal 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;
|
||||
59
components/Body/IFrameConnectTab/ShareModal.tsx
Normal file
59
components/Body/IFrameConnectTab/ShareModal.tsx
Normal 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;
|
||||
45
components/Body/IFrameConnectTab/SupportedDapps/DappTile.tsx
Normal file
45
components/Body/IFrameConnectTab/SupportedDapps/DappTile.tsx
Normal 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;
|
||||
@@ -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;
|
||||
147
components/Body/IFrameConnectTab/SupportedDapps/index.tsx
Normal file
147
components/Body/IFrameConnectTab/SupportedDapps/index.tsx
Normal 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;
|
||||
140
components/Body/IFrameConnectTab/index.tsx
Normal file
140
components/Body/IFrameConnectTab/index.tsx
Normal 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;
|
||||
72
components/Body/NetworkInput.tsx
Normal file
72
components/Body/NetworkInput.tsx
Normal 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;
|
||||
63
components/Body/NotificationBar.tsx
Normal file
63
components/Body/NotificationBar.tsx
Normal 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
48
components/Body/Tab.tsx
Normal 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;
|
||||
41
components/Body/TabsSelect.tsx
Normal file
41
components/Body/TabsSelect.tsx
Normal 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;
|
||||
90
components/Body/TenderlySettings.tsx
Normal file
90
components/Body/TenderlySettings.tsx
Normal 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;
|
||||
175
components/Body/TransactionRequests.tsx
Normal file
175
components/Body/TransactionRequests.tsx
Normal 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;
|
||||
38
components/Body/WalletConnectTab/ConnectionDetails.tsx
Normal file
38
components/Body/WalletConnectTab/ConnectionDetails.tsx
Normal 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;
|
||||
39
components/Body/WalletConnectTab/Loading.tsx
Normal file
39
components/Body/WalletConnectTab/Loading.tsx
Normal 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;
|
||||
90
components/Body/WalletConnectTab/URIInput.tsx
Normal file
90
components/Body/WalletConnectTab/URIInput.tsx
Normal 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;
|
||||
60
components/Body/WalletConnectTab/index.tsx
Normal file
60
components/Body/WalletConnectTab/index.tsx
Normal 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
701
components/Body/index.tsx
Normal 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;
|
||||
123
components/CustomConnectButton.tsx
Normal file
123
components/CustomConnectButton.tsx
Normal 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
227
components/Footer.tsx
Normal 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
48
components/Navbar.tsx
Normal 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;
|
||||
30
components/layouts/IndexLayout.tsx
Normal file
30
components/layouts/IndexLayout.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user