feat: move to nextjs
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user