"use client"; import { useState, useEffect, useCallback } from "react"; import { Container, useToast, Center, Spacer, Flex, VStack } from "@chakra-ui/react"; import { SingleValue } from "chakra-react-select"; // WC v2 import { Core } from "@walletconnect/core"; import { WalletKit, IWalletKit } from "@reown/walletkit"; 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 { useTransaction } from "../../contexts/TransactionContext"; import { useSmartWallet } from "../../contexts/SmartWalletContext"; import { TransactionExecutionMethod } from "../../types"; 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"; import WalletManager from "../SmartWallet/WalletManager"; import OwnerManagement from "../SmartWallet/OwnerManagement"; import WalletBalance from "../Balance/WalletBalance"; import TransactionApproval from "../TransactionExecution/TransactionApproval"; import TransactionBuilder from "../TransactionExecution/TransactionBuilder"; import TransactionHistory from "../TransactionExecution/TransactionHistory"; 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 || "demo-project-id", }); const primaryNetworkIds = [ 1, // ETH Mainnet 42161, // Arbitrum One 43114, // Avalanche 80094, // Berachain 56, // BSC 8453, // Base 250, // Fantom Opera 100, // Gnosis 10, // Optimism 137, // Polygon 130, // Unichain ]; 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() { let addressFromURL: string | null = null; let showAddressCache: string | null = null; let urlFromURL: string | null = null; let urlFromCache: string | null = null; let chainFromURL: string | null = null; let tenderlyForkIdCache: string | null = null; if (typeof window !== "undefined") { const urlParams = new URLSearchParams(window.location.search); addressFromURL = urlParams.get("address"); urlFromURL = urlParams.get("url"); chainFromURL = urlParams.get("chain"); } // Use sessionStorage for UI preferences (non-sensitive) if (typeof sessionStorage !== "undefined") { showAddressCache = sessionStorage.getItem("showAddress") ?? null; urlFromCache = sessionStorage.getItem("appUrl") ?? null; tenderlyForkIdCache = sessionStorage.getItem("tenderlyForkId") ?? null; } 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 { createTransaction, defaultExecutionMethod } = useTransaction(); const { activeWallet } = useSmartWallet(); const [provider, setProvider] = useState(); const [showAddress, setShowAddress] = useState( addressFromURL ?? showAddressCache ?? "" ); // gets displayed in input. ENS name remains as it is const [address, setAddress] = useState( addressFromURL ?? showAddressCache ?? "" ); // internal resolved address const [isAddressValid, setIsAddressValid] = useState(true); const [uri, setUri] = useState(""); const [networkId, setNetworkId] = useState(networkIdViaURL); const [selectedNetworkOption, setSelectedNetworkOption] = useState< SingleValue >({ label: networksList[networkIdViaURL].name, value: networkIdViaURL, }); // WC v2 const [web3wallet, setWeb3Wallet] = useState(); const [web3WalletSession, setWeb3WalletSession] = useState(); 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( urlFromURL ?? urlFromCache ?? undefined ); const [iframeKey, setIframeKey] = useState(0); // hacky way to reload iframe when key changes const [tenderlyForkId, setTenderlyForkId] = useState( tenderlyForkIdCache ?? "" ); const [sendTxnData, setSendTxnData] = useState([]); useEffect(() => { // only use cached address if no address from url provided if (!addressFromURL) { // getCachedSession - use sessionStorage for UI preferences const _showAddress = typeof sessionStorage !== "undefined" ? sessionStorage.getItem("showAddress") ?? undefined : undefined; // WC V2 initWeb3Wallet(true, _showAddress); } setProvider( new ethers.providers.JsonRpcProvider( `https://mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_KEY}` ) ); }, []); useEffect(() => { updateNetwork((selectedNetworkOption as SelectedNetworkOption).value); // eslint-disable-next-line }, [selectedNetworkOption]); useEffect(() => { if (provider && addressFromURL && urlFromURL) { initIFrame(); } // eslint-disable-next-line }, [provider]); useEffect(() => { // Use sessionStorage for UI preferences (non-sensitive) if (typeof sessionStorage !== "undefined") { sessionStorage.setItem("tenderlyForkId", tenderlyForkId); } }, [tenderlyForkId]); useEffect(() => { // Use sessionStorage for UI preferences (non-sensitive) if (typeof sessionStorage !== "undefined") { sessionStorage.setItem("showAddress", showAddress); } }, [showAddress]); useEffect(() => { // Use sessionStorage for UI preferences (non-sensitive) if (inputAppUrl && typeof sessionStorage !== "undefined") { sessionStorage.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: ethers.BigNumber.from("0x" + newTxn.value).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); toast({ title: "Txn Simulated on Tenderly", description: `Hash: ${res.data.result}`, status: "success", position: "bottom-right", duration: null, isClosable: true, }); }); } } // eslint-disable-next-line }, [latestTransaction, tenderlyForkId]); const initWeb3Wallet = async ( onlyIfActiveSessions?: boolean, _showAddress?: string ) => { const _web3wallet = await WalletKit.init({ core, metadata: WCMetadata, }); if (onlyIfActiveSessions) { const sessions = _web3wallet.getActiveSessions(); const sessionsArray = Object.values(sessions); console.log({ 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) && typeof sessionStorage !== "undefined") { sessionStorage.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 onSessionProposal = useCallback( async (proposal: { params: { requiredNamespaces: Record; optionalNamespaces?: Record }; id: number }) => { 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); }, [web3wallet] ); const handleSendTransaction = useCallback( async (id: number, params: any[], topic?: string) => { const txValue = params[0].value ? ethers.BigNumber.from(params[0].value).toString() : "0"; setSendTxnData((data) => { const newTxn = { id: id, from: params[0].from, to: params[0].to, data: params[0].data, value: txValue, }; if (data.some((d) => d.id === newTxn.id)) { return data; } else { return [newTxn, ...data]; } }); // If active smart wallet exists, create transaction for approval/execution if (activeWallet) { try { // Convert value properly using BigNumber const valueBigNumber = ethers.BigNumber.from(txValue); const valueHex = valueBigNumber.toHexString(); await createTransaction({ from: activeWallet.address, to: params[0].to, value: valueHex, data: params[0].data || "0x", method: defaultExecutionMethod, }); toast({ title: "Transaction Created", description: "Transaction added to approval queue", status: "info", isClosable: true, }); } catch (error: any) { toast({ title: "Transaction Creation Failed", description: error.message || "Failed to create transaction", status: "error", isClosable: true, }); } } // Handle execution method if (defaultExecutionMethod === TransactionExecutionMethod.SIMULATION && 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 }); toast({ title: "Txn Simulated on Tenderly", description: `Hash: ${res.result}`, status: "success", position: "bottom-right", duration: null, isClosable: true, }); } // Respond to WalletConnect if (web3wallet && topic) { if (activeWallet && defaultExecutionMethod !== TransactionExecutionMethod.SIMULATION) { // For now, return error - actual execution will be handled through approval flow await web3wallet.respondSessionRequest({ topic, response: { jsonrpc: "2.0", id: id, error: { code: 0, message: "Transaction queued for approval. Check Smart Wallet tab.", }, }, }); } else { await web3wallet.respondSessionRequest({ topic, response: { jsonrpc: "2.0", id: id, error: { code: 0, message: "Method not supported by Impersonator", }, }, }); } } }, [tenderlyForkId, web3wallet, activeWallet, createTransaction, defaultExecutionMethod, toast] ); const onSessionRequest = useCallback( async (event: { topic: string; params: { request: any }; id: number }) => { 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, handleSendTransaction] ); const onSessionDelete = () => { console.log("EVENT", "session_delete"); reset(); }; const subscribeToEvents = useCallback(async () => { console.log("ACTION", "subscribeToEvents"); if (web3wallet) { web3wallet.on("session_proposal", onSessionProposal); try { await web3wallet.core.pairing.pair({ uri }); } catch (e) { console.error(e); } web3wallet.on("session_request", onSessionRequest); web3wallet.on("session_delete", onSessionDelete); } }, [handleSendTransaction, web3wallet]); useEffect(() => { if (web3wallet) { subscribeToEvents(); } return () => { // Clean up event listeners if (web3wallet) { web3wallet.removeListener("session_proposal", onSessionProposal); web3wallet.removeListener("session_request", onSessionRequest); web3wallet.removeListener("session_delete", onSessionDelete); } }; }, [web3wallet, subscribeToEvents]); 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) { setWeb3WalletSession(undefined); setUri(""); setIsConnected(false); try { await web3wallet.disconnectSession({ topic: web3WalletSession.topic, reason: getSdkError("USER_DISCONNECTED"), }); } catch (e) { console.error("killSession", e); } } }; const reset = (persistUri?: boolean) => { setWeb3WalletSession(undefined); setIsConnected(false); if (!persistUri) { setUri(""); } localStorage.removeItem("walletconnect"); }; return ( <> {/* {process.env.NEXT_PUBLIC_GITCOIN_GRANTS_ACTIVE === "true" && ( )} */}
Connect to dapps as any ETH Address!
{(() => { switch (selectedTabIndex) { case 0: return ( ); case 1: return ( ); case 2: return ; case 3: return ( {activeWallet && ( <> )} ); } })()}
); } export default Body;