/ packages / frontend / src / testutils / mod.tsx
mod.tsx
  1  import React, { StrictMode } from "react";
  2  import {
  3    createMemoryHistory,
  4    createRootRoute,
  5    createRoute,
  6    createRouter,
  7    Outlet,
  8    RouterProvider,
  9  } from "@tanstack/react-router";
 10  import { createConfig, http, WagmiProvider } from "wagmi";
 11  import { connect } from "wagmi/actions";
 12  import { hardhat } from "wagmi/chains";
 13  import { mock } from "wagmi/connectors";
 14  import { createTestClient, publicActions, walletActions } from "viem";
 15  import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
 16  import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
 17  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 18  
 19  import { discoverRelay, RelayClient } from "@massmarket/client";
 20  import { mintShop } from "@massmarket/contracts";
 21  import { random256BigInt } from "@massmarket/utils";
 22  import StateManager from "@massmarket/stateManager";
 23  import { MemStore } from "@massmarket/store";
 24  
 25  import { MassMarketProvider } from "@massmarket/react-hooks";
 26  import { KeycardRole } from "../types.ts";
 27  import { env } from "../utils/env.ts";
 28  
 29  export const relayURL = Deno.env.get("RELAY_ENDPOINT") ||
 30    "http://localhost:4444/v4";
 31  const testRelayEndpoint = await discoverRelay(relayURL);
 32  
 33  export const testClient = createTestClient({
 34    transport: http(),
 35    chain: hardhat,
 36    mode: "anvil",
 37  })
 38    // Extend the client with public and wallet actions, so it can also act as a Public Client and Wallet Client
 39    .extend(publicActions)
 40    .extend(walletActions);
 41  const testAccounts = await testClient.requestAddresses();
 42  export const testAccount = testAccounts[0];
 43  
 44  export const connectors = [
 45    mock({
 46      accounts: [testAccount],
 47      features: {
 48        defaultConnected: true,
 49        reconnect: true,
 50      },
 51    }),
 52  ];
 53  
 54  export const createTestStateManager = async (shopId: bigint) => {
 55    const root = new Map(Object.entries({
 56      Tags: new Map(),
 57      Orders: new Map(),
 58      Accounts: new Map(),
 59      Inventory: new Map(),
 60      Listings: new Map(),
 61      Manifest: new Map(),
 62      SchemeVersion: 1,
 63    }));
 64    const stateManager = new StateManager({
 65      store: new MemStore(),
 66      id: shopId,
 67      defaultState: root,
 68    });
 69    await stateManager.open();
 70  
 71    return stateManager;
 72  };
 73  
 74  export const createTestRelayClient = async (
 75    shopId: bigint,
 76    enrollKeycard: boolean,
 77  ) => {
 78    const keyCardID = `keycard${shopId}`;
 79    const hasKC = localStorage.getItem(keyCardID);
 80    if (hasKC) {
 81      throw new Error("Keycard already exists");
 82    }
 83    const kcPrivateKey = generatePrivateKey();
 84    const keycard = privateKeyToAccount(kcPrivateKey);
 85  
 86    const relayClient = new RelayClient({
 87      relayEndpoint: testRelayEndpoint,
 88      walletClient: testClient,
 89      keycard,
 90      shopId,
 91    });
 92  
 93    if (enrollKeycard) {
 94      const result = await relayClient.enrollKeycard(
 95        testClient,
 96        testAccount,
 97        false,
 98      );
 99      if (!result.ok) {
100        throw new Error("Failed to enroll keycard");
101      }
102  
103      localStorage.setItem(
104        keyCardID,
105        JSON.stringify({
106          privateKey: kcPrivateKey,
107          role: KeycardRole.MERCHANT,
108        }),
109      );
110    }
111  
112    return relayClient;
113  };
114  
115  export const createRouterWrapper = async ({
116    shopId,
117    createShop = false,
118    enrollMerchant = false,
119    path = "/",
120    stateManager,
121    relayClient,
122  }: {
123    shopId?: bigint | null;
124    createShop?: boolean; // whether to mint a shop
125    enrollMerchant?: boolean; // whether to enroll a keycard
126    path?: string;
127    // The only case clientStateManager needs to be passed here is if we need access to the state manager before the router is created.
128    // For example, in EditListing_test.tsx, we need to access the state manager to create a new listing and then use the listing id to set the search param.
129    stateManager?: StateManager; // In most cases we don't need to pass clientStateManager separately.
130    relayClient?: RelayClient;
131  } = {}) => {
132    const config = createConfig({
133      chains: [hardhat], // testing only
134      transports: {
135        [hardhat.id]: http(),
136      },
137      connectors,
138    });
139    // establish wallet connection
140    await connect(config, { connector: config.connectors[0] });
141  
142    if (!shopId) {
143      shopId = random256BigInt();
144    }
145    if (createShop) {
146      const transactionHash = await mintShop(testClient, testAccount, [
147        shopId,
148        testAccount,
149      ]);
150      // this is still causing a leak
151      // https://github.com/wevm/viem/issues/2903
152      const receipt = await testClient.waitForTransactionReceipt({
153        hash: transactionHash,
154      });
155      if (receipt.status !== "success") {
156        throw new Error("Shop creation failed");
157      }
158    }
159    if (!relayClient) {
160      relayClient = await createTestRelayClient(shopId, enrollMerchant);
161    }
162  
163    if (!stateManager) {
164      stateManager = await createTestStateManager(shopId);
165    }
166  
167    const initialURL = (() => {
168      if (!shopId) return path;
169  
170      // parse it
171      const url = new URL(path, "http://localhost");
172      const searchParams = url.searchParams;
173      // override the shopId
174      searchParams.set("shopId", `0x${shopId.toString(16)}`);
175  
176      // Return the path with search params, but without the base URL
177      return `${url.pathname}${
178        searchParams.toString() ? "?" + searchParams.toString() : ""
179      }`;
180    })();
181  
182    const wrapper = ({ children }: { children: React.ReactNode }) => {
183      function RootComponent() {
184        return <Outlet />;
185      }
186      const rootRoute = createRootRoute({
187        component: RootComponent,
188      });
189      const componentRoute = createRoute({
190        getParentRoute: () => rootRoute,
191        path: "/",
192        component: () => <>{children}</>,
193      });
194      const createShopRoute = createRoute({
195        getParentRoute: () => rootRoute,
196        path: "/create-shop",
197        component: () => <>{children}</>,
198      });
199      const merchantConnectRoute = createRoute({
200        getParentRoute: () => rootRoute,
201        path: "/merchant-connect",
202        component: () => <>{children}</>,
203      });
204      const shippingRoute = createRoute({
205        getParentRoute: () => rootRoute,
206        path: "/shipping",
207        component: () => <>{children}</>,
208      });
209      const router = createRouter({
210        routeTree: rootRoute.addChildren([
211          componentRoute,
212          createShopRoute,
213          merchantConnectRoute,
214          shippingRoute,
215        ]),
216        history: createMemoryHistory({
217          initialEntries: [initialURL],
218        }),
219      });
220  
221      // Set initial data for wallet client
222      const queryClient = new QueryClient();
223      return (
224        <StrictMode>
225          <QueryClientProvider client={queryClient}>
226            <WagmiProvider config={config}>
227              <MassMarketProvider
228                stateManager={stateManager}
229                relayClient={relayClient}
230                config={env}
231              >
232                <RainbowKitProvider showRecentTransactions>
233                  {
234                    /* TS expects self closing RouterProvider tag. See App.tsx for how we are using it.
235              But if we use the self closing syntax in testing, the router functions don't work in testing environment. */
236                  }
237                  {/* @ts-expect-error */}
238                  <RouterProvider router={router}>{children}</RouterProvider>
239                </RainbowKitProvider>
240              </MassMarketProvider>
241            </WagmiProvider>
242          </QueryClientProvider>
243        </StrictMode>
244      );
245    };
246  
247    return {
248      wrapper,
249      stateManager,
250      relayClient,
251      testAccount,
252    };
253  };