@barnbridge/react-tenderly-fork-controls v0.0.5
React Tenderly Fork Controls
This package is can be added as part of your Web3 testing infrastructure. This package was inspired by impersonator.xyx and allows developers to impersonate wallet addresses while connected to a ganache or tenderly fork. The controls in this package allow you to set an address to impersonate, take snapshots of a fork, fast forward time, and revert the fork back to a previous snapshot.
We are using this package at https://beta.fiatdao.com/ as a way to test entering bond positions and fast forward time to maturity to test redemptions for any wallet address. Using snapshots have been very helpful to prevent needing to recreate several transactions to test one transactions of the bond lifecycle.
Usage
JSX/TSX Fork Controls
import { ForkControls } from 'react-tenderly-fork-controls';
import { useNetwork, useProvider, useSwitchNetwork } from 'wagmi';
import { JsonRpcProvider } from '@ethersproject/providers';
const component = () => {
const provider = useProvider() as JsonRpcProvider;
const { switchNetwork } = useSwitchNetwork();
const { chain } = useNetwork();
return (
<div>
<ForkControls
provider={provider}
forkType='tenderly' // or 'ganache'
chain={chain}
switchNetwork={switchNetwork} />
</div>
);
}
Hooks
useImpersonatingAddress
Get the address set by the impersonate button + modal
const impersonatingAddress = useImpersonatingAddress();
useEnableForkMode
Get true/false value if the toggle switch on the controls are on or off. This can be used to switch your wagmi client between a forked environment and a live chain.
const enableForkMode = useEnableForkMode();
useForkTimestamp
Returns current timestamp of where your fork is in time, and a method to requery the block timestamp.
const {forkTimestamp, getForkTimestamp} = useForkTimestamp();
Utility methods
createTenderlyfork
Asynchronous function to create a Tenderly fork using a post request to your tenderly rpc endpoint.
const tenderlyForkId = await createTenderlyFork(
tenderlyAccessKey,
tenderlyUser,
tenderlyProject);
controlsStore
This is a zustand store object and can be used to get and modify the internal state of the fork controls
The interface of the controls store:
interface ControlsState {
enableForkMode: boolean;
impersonatingAddress: string;
forkTimestamp: Date;
setEnableForkMode: (checked: boolean) => void;
setImpersonatingAddress: (address: string) => void;
getForkTimestamp: (provider: providers.JsonRpcProvider) => Promise<void>;
}
Usage:
import { controlsStore } from 'react-tenderly-fork-controls';
const enableForkMode = controlsStore.getState().enableForkMode;
const impersonatingAddress = controlsStore.getState().impersonatingAddress;
const forkTimestamp = controlsStore.getState().forkTimestamp;
const setEnableForkMode = controlsStore.getState().setEnableForkMode;
const setImpersonatingAddress = controlsStore.getState().setImpersonatingAddress;
const getForkTimestamp = controlsStore.getState().getForkTimestamp;
Next UI + Wagmi + RainbowKit Example Configuration
// Helper method that uses createTenderlyfork and initializes a jsonRPCProvider instance to the tenderly fork
const configureTenderly = async (config: wagmiClientConfig) => {
if (!config.tenderlyAccessKey || !config.tenderlyProject || !config.tenderlyUser) {
console.error('Requires tenderly api access configuration');
return null;
}
const tenderlyForkId = await createTenderlyFork(config.tenderlyAccessKey, config.tenderlyUser, config.tenderlyProject);
if (!tenderlyForkId) return;
const providerConfig = [jsonRpcProvider({
rpc: () => ({ http: `https://rpc.tenderly.co/fork/${tenderlyForkId}` })
})];
return providerConfig;
}
// Creates a jsonRPCProvider instance to a local ganache instance
const configureGanache = () => {
return [jsonRpcProvider({ rpc: () => ({ http: 'http://127.0.0.1:8545' })})]
}
// Creates a wagmi client using a mock wallet and connector with the address that is impersonating
const configureForkedEnv = async (config: wagmiClientConfig) => {
const chainConfig = [chain.localhost] as Chain[];
const providerConfig = config.useTenderly ? await configureTenderly(config) : configureGanache();
if (!providerConfig) return;
const { chains, provider, webSocketProvider } = configureChains(chainConfig, providerConfig);
const providerInstance = provider(({chainId: 1337})) as any;
const signer = config.impersonateAddress ? providerInstance?.getSigner(config.impersonateAddress) : undefined;
const mockWallet = (): Wallet => ({
createConnector: () => ({
connector: new MockConnector({
chains: [chain.localhost],
options: {
chainId: chain.localhost.id,
flags: {
failConnect: false,
failSwitchChain: false,
isAuthorized: true,
noSwitchChain: false,
},
signer,
},
}),
}),
id: 'mock',
iconBackground: 'tomato',
iconUrl: 'http://placekitten.com/100/100',
name: 'Mock Wallet',
});
const connectors = connectorsForWallets([
{ groupName: 'Fork And Impersonate', wallets: [mockWallet()] }
]);
return {
client: createClient({
autoConnect: signer ? true : false,
connectors,
provider,
webSocketProvider,
}),
chains,
}
}
// Creates a provider instance to a live chain
const configureLiveNetwork = (config: wagmiClientConfig) => {
const chainConfig = ((config.useTestnets) ? [chain.mainnet, chain.goerli] : [chain.mainnet]);
const { chains, provider, webSocketProvider } = configureChains(
chainConfig,
[alchemyProvider({ apiKey: config.alchemyAPIKey ?? "" })]
);
const { wallets } = getDefaultWallets({ appName: config.appName ?? "My Dapp", chains });
const connectors = connectorsForWallets([
...wallets,
{
groupName: 'Other',
wallets: [argentWallet({ chains }),
trustWallet({ chains }),
ledgerWallet({ chains })]
}
]);
return {
client: createClient({
autoConnect: false,
connectors,
provider,
webSocketProvider,
}),
chains
}
}
// Helper hook to allow switching between mainnet and forked envs
export const useWagmiClient = (config: wagmiClientConfig) => {
const [wagmiClient, setWagmiClient] = React.useState<any>({client: null, chains:[]});
React.useEffect(() => {
if (!config.useTenderly && !config.useGanache && config.alchemyAPIKey) {
const {client, chains} = configureLiveNetwork(config)
setWagmiClient({client, chains});
} else {
configureForkedEnv(config)
.then(setWagmiClient)
.catch(console.error);
}
}, [config]);
return wagmiClient;
}
function MyApp({ Component, pageProps }: AppProps) {
// Package hook usage
const impersonatingAddress = useImpersonatingAddress();
const enableForkMode = useEnableForkMode();
const config = React.useMemo(() => ({
useTenderly: enableForkMode, // Hook value
useGanache: false,
useTestnets: false,
impersonateAddress: impersonatingAddress, // Hook value
appName: APP_NAME,
alchemyAPIKey: ALCHEMY_API_KEY,
tenderlyAccessKey: TENDERLY_ACCESS_KEY,
tenderlyUser: TENDERLY_USER,
tenderlyProject: TENDERLY_PROJECT
}), [impersonatingAddress, enableForkMode]);
// Utility hook to manage switching between live chain and a fork
const {client: wagmiClient, chains} = useWagmiClient(config);
if (!wagmiClient) return <></>;
return (
<>
<Head>
<title>My App</title>
</Head>
<WagmiConfig client={wagmiClient}>
<RainbowKitProvider chains={chains}>
<Component {...pageProps} />
</RainbowKitProvider>
</WagmiConfig>
</>
);
}