Skip to content

Commit

Permalink
set up account kit in react template
Browse files Browse the repository at this point in the history
  • Loading branch information
holic committed Jun 1, 2024
1 parent 65b4bd5 commit 9160f5e
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 172 deletions.
6 changes: 5 additions & 1 deletion templates/react/packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@
"test": "tsc --noEmit"
},
"dependencies": {
"@latticexyz/account-kit": "link:../../../../packages/account-kit",
"@latticexyz/common": "link:../../../../packages/common",
"@latticexyz/dev-tools": "link:../../../../packages/dev-tools",
"@latticexyz/react": "link:../../../../packages/react",
"@latticexyz/schema-type": "link:../../../../packages/schema-type",
"@latticexyz/store-sync": "link:../../../../packages/store-sync",
"@latticexyz/utils": "link:../../../../packages/utils",
"@latticexyz/world": "link:../../../../packages/world",
"@rainbow-me/rainbowkit": "^2.0.2",
"@tanstack/react-query": "^5.28.8",
"contracts": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rxjs": "7.5.5",
"viem": "2.9.20"
"viem": "2.9.20",
"wagmi": "^2.8.0"
},
"devDependencies": {
"@types/react": "18.2.22",
Expand Down
153 changes: 80 additions & 73 deletions templates/react/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMUD } from "./MUDContext";
import { AccountButton, useAppAccountClient } from "@latticexyz/account-kit";

const styleUnset = { all: "unset" } as const;

Expand All @@ -7,6 +8,7 @@ export const App = () => {
network: { tables, useStore },
systemCalls: { addTask, toggleTask, deleteTask },
} = useMUD();
const appAccountClient = useAppAccountClient();

const tasks = useStore((state) => {
const records = Object.values(state.getRecords(tables.Tasks));
Expand All @@ -16,90 +18,95 @@ export const App = () => {

return (
<>
<table>
<tbody>
{tasks.map((task) => (
<tr key={task.id}>
<td align="right">
<input
type="checkbox"
checked={task.value.completedAt > 0n}
title={task.value.completedAt === 0n ? "Mark task as completed" : "Mark task as incomplete"}
onChange={async (event) => {
event.preventDefault();
const checkbox = event.currentTarget;
<AccountButton />
<br />
<br />
{appAccountClient ? (
<table>
<tbody>
{tasks.map((task) => (
<tr key={task.id}>
<td align="right">
<input
type="checkbox"
checked={task.value.completedAt > 0n}
title={task.value.completedAt === 0n ? "Mark task as completed" : "Mark task as incomplete"}
onChange={async (event) => {
event.preventDefault();
const checkbox = event.currentTarget;

checkbox.disabled = true;
try {
await toggleTask(task.key.id);
} finally {
checkbox.disabled = false;
}
}}
/>
checkbox.disabled = true;
try {
await toggleTask(appAccountClient, task.key.id);
} finally {
checkbox.disabled = false;
}
}}
/>
</td>
<td>{task.value.completedAt > 0n ? <s>{task.value.description}</s> : <>{task.value.description}</>}</td>
<td align="right">
<button
type="button"
title="Delete task"
style={styleUnset}
onClick={async (event) => {
event.preventDefault();
if (!window.confirm("Are you sure you want to delete this task?")) return;

const button = event.currentTarget;
button.disabled = true;
try {
await deleteTask(appAccountClient, task.key.id);
} finally {
button.disabled = false;
}
}}
>
&times;
</button>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td>
<input type="checkbox" disabled />
</td>
<td>{task.value.completedAt > 0n ? <s>{task.value.description}</s> : <>{task.value.description}</>}</td>
<td align="right">
<button
type="button"
title="Delete task"
style={styleUnset}
onClick={async (event) => {
<td colSpan={2}>
<form
onSubmit={async (event) => {
event.preventDefault();
if (!window.confirm("Are you sure you want to delete this task?")) return;
const form = event.currentTarget;
const fieldset = form.querySelector("fieldset");
if (!(fieldset instanceof HTMLFieldSetElement)) return;

const formData = new FormData(form);
const desc = formData.get("description");
if (typeof desc !== "string") return;

const button = event.currentTarget;
button.disabled = true;
fieldset.disabled = true;
try {
await deleteTask(task.key.id);
await addTask(appAccountClient, desc);
form.reset();
} finally {
button.disabled = false;
fieldset.disabled = false;
}
}}
>
&times;
</button>
<fieldset style={styleUnset}>
<input type="text" name="description" />{" "}
<button type="submit" title="Add task">
Add
</button>
</fieldset>
</form>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td>
<input type="checkbox" disabled />
</td>
<td colSpan={2}>
<form
onSubmit={async (event) => {
event.preventDefault();
const form = event.currentTarget;
const fieldset = form.querySelector("fieldset");
if (!(fieldset instanceof HTMLFieldSetElement)) return;

const formData = new FormData(form);
const desc = formData.get("description");
if (typeof desc !== "string") return;

fieldset.disabled = true;
try {
await addTask(desc);
form.reset();
} finally {
fieldset.disabled = false;
}
}}
>
<fieldset style={styleUnset}>
<input type="text" name="description" />{" "}
<button type="submit" title="Add task">
Add
</button>
</fieldset>
</form>
</td>
</tr>
</tfoot>
</table>
</tfoot>
</table>
) : null}
</>
);
};
80 changes: 62 additions & 18 deletions templates/react/packages/client/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
import "./polyfills";
import "@rainbow-me/rainbowkit/styles.css";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import { setup } from "./mud/setup";
import { MUDProvider } from "./MUDContext";
import mudConfig from "contracts/mud.config";
// import mudConfig from "contracts/mud.config";
import { WagmiProvider, createConfig } from "wagmi";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { supportedChains } from "./mud/supportedChains";
import { createClient, http } from "viem";
import { getNetworkConfig } from "./mud/getNetworkConfig";
import { RainbowKitProvider, lightTheme, midnightTheme } from "@rainbow-me/rainbowkit";
import { AccountKitProvider } from "@latticexyz/account-kit";

const queryClient = new QueryClient();

const wagmiConfig = createConfig({
chains: supportedChains,
client: ({ chain }) =>
createClient({
chain,
// We intentionally don't use fallback+webSocket here because if a chain's RPC config
// doesn't include a `webSocket` entry, it doesn't seem to fallback and instead just
// ~never makes any requests and all queries seem to sit idle.
transport: http(),
}),
});

const networkConfig = getNetworkConfig();

const rootElement = document.getElementById("react-root");
if (!rootElement) throw new Error("React root not found");
Expand All @@ -11,24 +36,43 @@ const root = ReactDOM.createRoot(rootElement);
// TODO: figure out if we actually want this to be async or if we should render something else in the meantime
setup().then(async (result) => {
root.render(
<MUDProvider value={result}>
<App />
</MUDProvider>,
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={{
lightMode: lightTheme({ borderRadius: "none" }),
darkMode: midnightTheme({ borderRadius: "none" }),
}}
>
<AccountKitProvider
config={{
chainId: networkConfig.chain.id,
worldAddress: networkConfig.worldAddress,
erc4337: false,
}}
>
<MUDProvider value={result}>
<App />
</MUDProvider>
</AccountKitProvider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>,
);

// https://vitejs.dev/guide/env-and-mode.html
if (import.meta.env.DEV) {
const { mount: mountDevTools } = await import("@latticexyz/dev-tools");
mountDevTools({
config: mudConfig,
publicClient: result.network.publicClient,
walletClient: result.network.walletClient,
latestBlock$: result.network.latestBlock$,
storedBlockLogs$: result.network.storedBlockLogs$,
worldAddress: result.network.worldContract.address,
worldAbi: result.network.worldContract.abi,
write$: result.network.write$,
useStore: result.network.useStore,
});
}
// if (import.meta.env.DEV) {
// const { mount: mountDevTools } = await import("@latticexyz/dev-tools");
// mountDevTools({
// config: mudConfig,
// publicClient: result.network.publicClient,
// walletClient: result.network.walletClient,
// latestBlock$: result.network.latestBlock$,
// storedBlockLogs$: result.network.storedBlockLogs$,
// worldAddress: result.network.worldContract.address,
// worldAbi: result.network.worldContract.abi,
// write$: result.network.write$,
// useStore: result.network.useStore,
// });
// }
});
72 changes: 37 additions & 35 deletions templates/react/packages/client/src/mud/createSystemCalls.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,52 @@
/*
* Create the system calls that the client can use to ask
* for changes in the World state (using the System contracts).
*/

import { Hex } from "viem";
import { Account, Chain, Client, Hex, Transport } from "viem";
import { writeContract } from "viem/actions";
import { SetupNetworkResult } from "./setupNetwork";
import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";

export type SystemCalls = ReturnType<typeof createSystemCalls>;

export function createSystemCalls(
/*
* The parameter list informs TypeScript that:
*
* - The first parameter is expected to be a
* SetupNetworkResult, as defined in setupNetwork.ts
*
* Out of this parameter, we only care about two fields:
* - worldContract (which comes from getContract, see
* https://github.com/latticexyz/mud/blob/main/templates/react/packages/client/src/mud/setupNetwork.ts#L63-L69).
*
* - waitForTransaction (which comes from syncToRecs, see
* https://github.com/latticexyz/mud/blob/main/templates/react/packages/client/src/mud/setupNetwork.ts#L77-L83).
*
* - From the second parameter, which is a ClientComponent,
* we only care about Counter. This parameter comes to use
* through createClientComponents.ts, but it originates in
* syncToRecs
* (https://github.com/latticexyz/mud/blob/main/templates/react/packages/client/src/mud/setupNetwork.ts#L77-L83).
*/
{ tables, useStore, worldContract, waitForTransaction }: SetupNetworkResult,
) {
const addTask = async (label: string) => {
const tx = await worldContract.write.app__addTask([label]);
export function createSystemCalls({ tables, useStore, waitForTransaction, worldAddress }: SetupNetworkResult) {
const addTask = async (client: Client<Transport, Chain, Account> | undefined, label: string) => {
if (!client) throw new Error("Not connected");

const tx = await writeContract(client, {
address: worldAddress,
abi: IWorldAbi,
functionName: "app__addTask",
args: [label],
});
await waitForTransaction(tx);
};

const toggleTask = async (id: Hex) => {
const toggleTask = async (client: Client<Transport, Chain, Account> | undefined, id: Hex) => {
if (!client) throw new Error("Not connected");

const isComplete = (useStore.getState().getValue(tables.Tasks, { id })?.completedAt ?? 0n) > 0n;
const tx = isComplete
? await worldContract.write.app__resetTask([id])
: await worldContract.write.app__completeTask([id]);
? await writeContract(client, {
address: worldAddress,
abi: IWorldAbi,
functionName: "app__resetTask",
args: [id],
})
: await writeContract(client, {
address: worldAddress,
abi: IWorldAbi,
functionName: "app__completeTask",
args: [id],
});
await waitForTransaction(tx);
};

const deleteTask = async (id: Hex) => {
const tx = await worldContract.write.app__deleteTask([id]);
const deleteTask = async (client: Client<Transport, Chain, Account> | undefined, id: Hex) => {
if (!client) throw new Error("Not connected");

const tx = await writeContract(client, {
address: worldAddress,
abi: IWorldAbi,
functionName: "app__deleteTask",
args: [id],
});
await waitForTransaction(tx);
};

Expand Down
Loading

0 comments on commit 9160f5e

Please sign in to comment.