0.0.5 • Published 1 year ago

@barnbridge/react-tenderly-fork-controls v0.0.5

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

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>
    </>
  );
}
0.0.5

1 year ago

0.0.4

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago