Skip to content

Commit

Permalink
docs(create-mud): explain boilerplate in templates (#1427)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <[email protected]>
  • Loading branch information
qbzzt and holic authored Sep 12, 2023
1 parent 2ca75f9 commit d2c8488
Show file tree
Hide file tree
Showing 23 changed files with 645 additions and 8 deletions.
11 changes: 11 additions & 0 deletions templates/phaser/packages/client/src/mud/createClientComponents.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
/*
* Creates components for use by the client.
*
* By default it returns the components from setupNetwork.ts, those which are
* automatically inferred from the mud.config.ts table definitions.
*
* However, you can add or override components here as needed. This
* lets you add user defined components, which may or may not have
* an onchain component.
*/

import { SetupNetworkResult } from "./setupNetwork";

export type ClientComponents = ReturnType<typeof createClientComponents>;
Expand Down
34 changes: 34 additions & 0 deletions templates/phaser/packages/client/src/mud/createSystemCalls.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
/*
* Creates components for use by the client.
*
* By default it returns the components from setupNetwork.ts, those which are
* automatically inferred from the mud.config.ts table definitions.
*
* However, you can add or override components here as needed. This
* lets you add user defined components, which may or may not have
* an onchain component.
*/

import { getComponentValue } from "@latticexyz/recs";
import { ClientComponents } from "./createClientComponents";
import { SetupNetworkResult } from "./setupNetwork";
Expand All @@ -6,10 +17,33 @@ import { singletonEntity } from "@latticexyz/store-sync/recs";
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 createContract, see
* https://github.com/latticexyz/mud/blob/26dabb34321eedff7a43f3fcb46da4f3f5ba3708/templates/phaser/packages/client/src/mud/setupNetwork.ts#L31).
* - waitForTransaction (which comes from syncToRecs, see
* https://github.com/latticexyz/mud/blob/26dabb34321eedff7a43f3fcb46da4f3f5ba3708/templates/phaser/packages/client/src/mud/setupNetwork.ts#L39).
*
* - 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/26dabb34321eedff7a43f3fcb46da4f3f5ba3708/templates/phaser/packages/client/src/mud/setupNetwork.ts#L39).
*/
{ worldContract, waitForTransaction }: SetupNetworkResult,
{ Counter }: ClientComponents
) {
const increment = async () => {
/*
* Because IncrementSystem
* (https://mud.dev/tutorials/walkthrough/minimal-onchain#incrementsystemsol)
* is in the root namespace, `.increment` can be called directly
* on the World contract.
*/
const tx = await worldContract.write.increment();
await waitForTransaction(tx);
return getComponentValue(Counter, singletonEntity);
Expand Down
58 changes: 58 additions & 0 deletions templates/phaser/packages/client/src/mud/getNetworkConfig.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,80 @@
/*
* Network specific configuration for the client.
* By default connect to the anvil test network.
*/

/*
* By default the template just creates a temporary wallet
* (called a burner wallet) and uses a faucet (on our test net)
* to get ETH for it.
*
* See https://mud.dev/tutorials/minimal/deploy#wallet-managed-address
* for how to use the user's own address instead.
*/
import { getBurnerPrivateKey } from "@latticexyz/common";

/*
* Import the addresses of the World, possibly on multiple chains,
* from packages/contracts/worlds.json. When the contracts package
* deploys a new `World`, it updates this file.
*/
import worlds from "contracts/worlds.json";

/*
* The supported chains.
* By default, there are only two chains here:
*
* - mudFoundry, the chain running on anvil that pnpm dev
* starts by default. It is similar to the viem anvil chain
* (see https://viem.sh/docs/clients/test.html), but with the
* basefee set to zero to avoid transaction fees.
* - latticeTestnet, our public test network.
*
* See https://mud.dev/tutorials/minimal/deploy#run-the-user-interface
* for instructions on how to add networks.
*/
import { supportedChains } from "./supportedChains";

export async function getNetworkConfig() {
const params = new URLSearchParams(window.location.search);

/*
* The chain ID is the first item available from this list:
* 1. chainId query parameter
* 2. chainid query parameter
* 3. The VITE_CHAIN_ID environment variable set when the
* vite dev server was started or client was built
* 4. The default, 31337 (anvil)
*/
const chainId = Number(params.get("chainId") || params.get("chainid") || import.meta.env.VITE_CHAIN_ID || 31337);

/*
* Find the chain (unless it isn't in the list of supported chains).
*/
const chainIndex = supportedChains.findIndex((c) => c.id === chainId);
const chain = supportedChains[chainIndex];
if (!chain) {
throw new Error(`Chain ${chainId} not found`);
}

/*
* Get the address of the World. If you want to use a
* different address than the one in worlds.json,
* provide it as worldAddress in the query string.
*/
const world = worlds[chain.id.toString()];
const worldAddress = params.get("worldAddress") || world?.address;
if (!worldAddress) {
throw new Error(`No world address found for chain ${chainId}. Did you run \`mud deploy\`?`);
}

/*
* MUD clients use events to synchronize the database, meaning
* they need to look as far back as when the World was started.
* The block number for the World start can be specified either
* on the URL (as initialBlockNumber) or in the worlds.json
* file. If neither has it, it starts at the first block, zero.
*/
const initialBlockNumber = params.has("initialBlockNumber")
? Number(params.get("initialBlockNumber"))
: world?.blockNumber ?? 0n;
Expand Down
42 changes: 41 additions & 1 deletion templates/phaser/packages/client/src/mud/setupNetwork.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* The MUD client code is built on top of viem
* (https://viem.sh/docs/getting-started.html).
* This line imports the functions we need from it.
*/
import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig } from "viem";
import { createFaucetService } from "@latticexyz/services/faucet";
import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs";
Expand All @@ -6,13 +11,26 @@ import { world } from "./world";
import IWorldAbi from "contracts/abi/IWorld.sol/IWorld.abi.json";
import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common";
import { Subject, share } from "rxjs";

/*
* Import our MUD config, which includes strong types for
* our tables and other config options. We use this to generate
* things like RECS components and get back strong types for them.
*
* See https://mud.dev/tutorials/walkthrough/minimal-onchain#mudconfigts
* for the source of this information.
*/
import mudConfig from "contracts/mud.config";

export type SetupNetworkResult = Awaited<ReturnType<typeof setupNetwork>>;

export async function setupNetwork() {
const networkConfig = await getNetworkConfig();

/*
* Create a viem public (read only) client
* (https://viem.sh/docs/clients/public.html)
*/
const clientOptions = {
chain: networkConfig.chain,
transport: transportObserver(fallback([webSocket(), http()])),
Expand All @@ -21,13 +39,25 @@ export async function setupNetwork() {

const publicClient = createPublicClient(clientOptions);

/*
* Create a temporary wallet and a viem client for it
* (see https://viem.sh/docs/clients/wallet.html).
*/
const burnerAccount = createBurnerAccount(networkConfig.privateKey as Hex);
const burnerWalletClient = createWalletClient({
...clientOptions,
account: burnerAccount,
});

/*
* Create an observable for contract writes that we can
* pass into MUD dev tools for transaction observability.
*/
const write$ = new Subject<ContractWrite>();

/*
* Create an object for communicating with the deployed World.
*/
const worldContract = createContract({
address: networkConfig.worldAddress as Hex,
abi: IWorldAbi,
Expand All @@ -36,6 +66,12 @@ export async function setupNetwork() {
onWrite: (write) => write$.next(write),
});

/*
* Sync on-chain state into RECS and keeps our client in sync.
* Uses the MUD indexer if available, otherwise falls back
* to the viem publicClient to make RPC calls to fetch MUD
* events from the chain.
*/
const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({
world,
config: mudConfig,
Expand All @@ -44,7 +80,11 @@ export async function setupNetwork() {
startBlock: BigInt(networkConfig.initialBlockNumber),
});

// Request drip from faucet
/*
* If there is a faucet, request (test) ETH if you have
* less than 1 ETH. Repeat every 20 seconds to ensure you don't
* run out.
*/
if (networkConfig.faucetServiceUrl) {
const address = burnerAccount.address;
console.info("[Dev Faucet]: Player address -> ", address);
Expand Down
17 changes: 16 additions & 1 deletion templates/phaser/packages/client/src/mud/supportedChains.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
/*
* The supported chains.
* By default, there are only two chains here:
*
* - mudFoundry, the chain running on anvil that pnpm dev
* starts by default. It is similar to the viem anvil chain
* (see https://viem.sh/docs/clients/test.html), but with the
* basefee set to zero to avoid transaction fees.
* - latticeTestnet, our public test network.
*
*/

import { MUDChain, latticeTestnet, mudFoundry } from "@latticexyz/common/chains";

// If you are deploying to chains other than anvil or Lattice testnet, add them here
/*
* See https://mud.dev/tutorials/minimal/deploy#run-the-user-interface
* for instructions on how to add networks.
*/
export const supportedChains: MUDChain[] = [mudFoundry, latticeTestnet];
11 changes: 11 additions & 0 deletions templates/react/packages/client/src/mud/createClientComponents.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
/*
* Creates components for use by the client.
*
* By default it returns the components from setupNetwork.ts, those which are
* automatically inferred from the mud.config.ts table definitions.
*
* However, you can add or override components here as needed. This
* lets you add user defined components, which may or may not have
* an onchain component.
*/

import { SetupNetworkResult } from "./setupNetwork";

export type ClientComponents = ReturnType<typeof createClientComponents>;
Expand Down
28 changes: 28 additions & 0 deletions templates/react/packages/client/src/mud/createSystemCalls.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* Create the system calls that the client can use to ask
* for changes in the World state (using the System contracts).
*/

import { getComponentValue } from "@latticexyz/recs";
import { ClientComponents } from "./createClientComponents";
import { SetupNetworkResult } from "./setupNetwork";
Expand All @@ -6,10 +11,33 @@ import { singletonEntity } from "@latticexyz/store-sync/recs";
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 createContract, see
* https://github.com/latticexyz/mud/blob/26dabb34321eedff7a43f3fcb46da4f3f5ba3708/templates/react/packages/client/src/mud/setupNetwork.ts#L31).
* - waitForTransaction (which comes from syncToRecs, see
* https://github.com/latticexyz/mud/blob/26dabb34321eedff7a43f3fcb46da4f3f5ba3708/templates/react/packages/client/src/mud/setupNetwork.ts#L39).
*
* - 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/26dabb34321eedff7a43f3fcb46da4f3f5ba3708/templates/react/packages/client/src/mud/setupNetwork.ts#L39).
*/
{ worldContract, waitForTransaction }: SetupNetworkResult,
{ Counter }: ClientComponents
) {
const increment = async () => {
/*
* Because IncrementSystem
* (https://mud.dev/tutorials/walkthrough/minimal-onchain#incrementsystemsol)
* is in the root namespace, `.increment` can be called directly
* on the World contract.
*/
const tx = await worldContract.write.increment();
await waitForTransaction(tx);
return getComponentValue(Counter, singletonEntity);
Expand Down
59 changes: 59 additions & 0 deletions templates/react/packages/client/src/mud/getNetworkConfig.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,81 @@
/*
* Network specific configuration for the client.
* By default connect to the anvil test network.
*
*/

/*
* By default the template just creates a temporary wallet
* (called a burner wallet) and uses a faucet (on our test net)
* to get ETH for it.
*
* See https://mud.dev/tutorials/minimal/deploy#wallet-managed-address
* for how to use the user's own address instead.
*/
import { getBurnerPrivateKey } from "@latticexyz/common";

/*
* Import the addresses of the World, possibly on multiple chains,
* from packages/contracts/worlds.json. When the contracts package
* deploys a new `World`, it updates this file.
*/
import worlds from "contracts/worlds.json";

/*
* The supported chains.
* By default, there are only two chains here:
*
* - mudFoundry, the chain running on anvil that pnpm dev
* starts by default. It is similar to the viem anvil chain
* (see https://viem.sh/docs/clients/test.html), but with the
* basefee set to zero to avoid transaction fees.
* - latticeTestnet, our public test network.
*
* See https://mud.dev/tutorials/minimal/deploy#run-the-user-interface
* for instructions on how to add networks.
*/
import { supportedChains } from "./supportedChains";

export async function getNetworkConfig() {
const params = new URLSearchParams(window.location.search);

/*
* The chain ID is the first item available from this list:
* 1. chainId query parameter
* 2. chainid query parameter
* 3. The VITE_CHAIN_ID environment variable set when the
* vite dev server was started or client was built
* 4. The default, 31337 (anvil)
*/
const chainId = Number(params.get("chainId") || params.get("chainid") || import.meta.env.VITE_CHAIN_ID || 31337);

/*
* Find the chain (unless it isn't in the list of supported chains).
*/
const chainIndex = supportedChains.findIndex((c) => c.id === chainId);
const chain = supportedChains[chainIndex];
if (!chain) {
throw new Error(`Chain ${chainId} not found`);
}

/*
* Get the address of the World. If you want to use a
* different address than the one in worlds.json,
* provide it as worldAddress in the query string.
*/
const world = worlds[chain.id.toString()];
const worldAddress = params.get("worldAddress") || world?.address;
if (!worldAddress) {
throw new Error(`No world address found for chain ${chainId}. Did you run \`mud deploy\`?`);
}

/*
* MUD clients use events to synchronize the database, meaning
* they need to look as far back as when the World was started.
* The block number for the World start can be specified either
* on the URL (as initialBlockNumber) or in the worlds.json
* file. If neither has it, it starts at the first block, zero.
*/
const initialBlockNumber = params.has("initialBlockNumber")
? Number(params.get("initialBlockNumber"))
: world?.blockNumber ?? 0n;
Expand Down
Loading

0 comments on commit d2c8488

Please sign in to comment.