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 };