Files
impersonator/components/Body/index.tsx
defiQUG 55fe7d10eb feat: comprehensive project improvements and fixes
- Fix all TypeScript compilation errors (40+ fixes)
  - Add missing type definitions (TransactionRequest, SafeInfo)
  - Fix TransactionRequestStatus vs TransactionStatus confusion
  - Fix import paths and provider type issues
  - Fix test file errors and mock providers

- Implement comprehensive security features
  - AES-GCM encryption with PBKDF2 key derivation
  - Input validation and sanitization
  - Rate limiting and nonce management
  - Replay attack prevention
  - Access control and authorization

- Add comprehensive test suite
  - Integration tests for transaction flow
  - Security validation tests
  - Wallet management tests
  - Encryption and rate limiter tests
  - E2E tests with Playwright

- Add extensive documentation
  - 12 numbered guides (setup, development, API, security, etc.)
  - Security documentation and audit reports
  - Code review and testing reports
  - Project organization documentation

- Update dependencies
  - Update axios to latest version (security fix)
  - Update React types to v18
  - Fix peer dependency warnings

- Add development tooling
  - CI/CD workflows (GitHub Actions)
  - Pre-commit hooks (Husky)
  - Linting and formatting (Prettier, ESLint)
  - Security audit workflow
  - Performance benchmarking

- Reorganize project structure
  - Move reports to docs/reports/
  - Clean up root directory
  - Organize documentation

- Add new features
  - Smart wallet management (Gnosis Safe, ERC4337)
  - Transaction execution and approval workflows
  - Balance management and token support
  - Error boundary and monitoring (Sentry)

- Fix WalletConnect configuration
  - Handle missing projectId gracefully
  - Add environment variable template
2026-01-14 02:17:26 -08:00

829 lines
24 KiB
TypeScript

"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<ethers.providers.JsonRpcProvider>();
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<SelectedNetworkOption>
>({
label: networksList[networkIdViaURL].name,
value: networkIdViaURL,
});
// WC v2
const [web3wallet, setWeb3Wallet] = useState<IWalletKit>();
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(
tenderlyForkIdCache ?? ""
);
const [sendTxnData, setSendTxnData] = useState<TxnDataType[]>([]);
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<string, ProposalTypes.BaseRequiredNamespace>; optionalNamespaces?: Record<string, any> }; 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" && (
<NotificationBar />
)} */}
<NotificationBar />
<Center mt="8" fontStyle={"italic"}>
Connect to dapps as any ETH Address!
</Center>
<Container mt="2" 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 />;
case 3:
return (
<VStack spacing={6} mt={4} align="stretch">
<WalletManager />
{activeWallet && (
<>
<OwnerManagement />
<WalletBalance />
<TransactionBuilder />
<TransactionApproval />
<TransactionHistory />
</>
)}
</VStack>
);
}
})()}
<Center>
<TransactionRequests
sendTxnData={sendTxnData}
setSendTxnData={setSendTxnData}
networkId={networkId}
/>
</Center>
</Container>
</>
);
}
export default Body;