Deploy to a blockchain

In this tutorial you deploy the application to a public blockchain, either the Lattice test blockchain or a general purpose one.

Setup

  1. Create a new MUD application from the template, but do not start it.

Deploying to the Lattice test blockchain

  1. Deploy the contracts. The deployment should automatically call the faucet with the autogenerated address to get test ETH.

    cd packages/contracts
    pnpm deploy:testnet
    cd ../..
  2. Specify the chain ID for the blockchain.

    export VITE_CHAIN_ID=4242

    Note that you can also do this from the browser, by appending ?chainID=<chainID> to the user interface URL.

  3. Run the user interface.

    cd packages/client
    pnpm vite
  4. Browse to the URL for the application.

  5. You can view transactions on the Otter explorer (opens in a new tab). The addresses for the World and the user account are available in the MUD dev tool.

Deploying to a third party blockchain (Sepolia, Optimism Goerli, etc.)

To use a third party blockchain you need:

  • URLs for both HTTP and WebSocket.
  • Address (and the private key for it)
  • ETH on that address. In the case of a test blockchain, there is usually a faucet you can use.

Deploy the contracts

  1. Edit the environment file for the contracts.

    packages/contracts/.env
    PRIVATE_KEY=<your private key>
  2. Add a section to the Foundry configuration file:

    packages/contracts/foundry.toml
    [profile.mychain]
    eth_rpc_url = " <your HTTPS URL goes here> "
  3. Deploy the contracts.

    cd packages/contracts
     
    pnpm mud deploy --profile=mychain
    cd ../..

Run the user interface

  1. Edit the supported chains list to add the blockchain you use. To know get the blockchain name to import, see the list of supported blockchains (opens in a new tab). If your blockchain is not supported, see the directions on creating a custom blockchain (opens in a new tab).

    packages/client/src/mud/supportedChains.ts
    import { MUDChain, latticeTestnet, mudFoundry } from "@latticexyz/common/chains";
    import { <network> } from "viem/chains";
     
    <network>.rpcUrls.default.http = [ '<URL to access the blockchain through HTTP(S)>' ]
    <network>.rpcUrls.default.webSocket = [ '<URL to access the blockchain through WebSocket>' ]
     
    // If you are deploying to chains other than anvil or Lattice testnet, add them here
    export const supportedChains: MUDChain[] = [mudFoundry, latticeTestnet, sepolia];
  2. Specify the chain ID for the blockchain you are using

    export VITE_CHAIN_ID=<chain ID for the blockchain you use>

    Note that you can also do this from the browser, by appending ?chainID=<chainID> to the user interface URL.

  3. Run the user interface.

    cd packages/clients
    pnpm vite
  4. Browse to the URL for the application.

Use the user interface

You can already read information (the value of Counter, for example) with the user interface. However, to issue transactions, for example to increment the counter, you need ETH. There are two options here:

Application-managed address

This is the default setting for the template. The application creates a private key, and uses that to interact with the blockchain. The advantage is that as there is no wallet software, the user does not need to manually approve each transaction. However, this means that the transactions are issued by a random account with no ETH.

To enable the use of a blockchain that does have gas costs, get the address of the account from the MUD dev tools and send it some testnet ETH.

Wallet-managed address

The other option is to replace the burner wallet by the user's own wallet, and accept that transactions will need to be signed. This is the strategy used, for example, by Words3 (opens in a new tab). To configure this, you need to edit the setupNetworks.ts file.

packages/client/src/mud/setupNetwork.ts
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";
import { getNetworkConfig } from "./getNetworkConfig";
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 mudConfig from "contracts/mud.config";
import { createWalletClient, custom } from 'viem';
import { <network> } from 'viem/chains'
 
export type SetupNetworkResult = Awaited<ReturnType<typeof setupNetwork>>;
 
export async function setupNetwork() {
  const networkConfig = await getNetworkConfig();
 
  const clientOptions = {
    chain: networkConfig.chain,
    transport: transportObserver(fallback([webSocket(), http()])),
    pollingInterval: 1000,
  } as const satisfies ClientConfig;
 
  const publicClient = createPublicClient(clientOptions);
 
  const burnerAccount = createBurnerAccount(networkConfig.privateKey as Hex);
  const burnerWalletClient = createWalletClient({
    ...clientOptions,
    account: burnerAccount,
  });
 
  const walletClient = createWalletClient({
    chain: <network>,
    transport: custom(window.ethereum)
  })
  const accounts = await walletClient.requestAddresses()
  walletClient.account = {address: accounts[0]}
 
  const write$ = new Subject<ContractWrite>();
  const worldContract = createContract({
    address: networkConfig.worldAddress as Hex,
    abi: IWorldAbi,
    publicClient,
    walletClient,
    onWrite: (write) => write$.next(write),
  });
 
 
  const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({
    world,
    config: mudConfig,
    address: networkConfig.worldAddress as Hex,
    publicClient,
    startBlock: BigInt(networkConfig.initialBlockNumber),
  });
 
  // Request drip from faucet
  if (networkConfig.faucetServiceUrl) {
    const address = burnerAccount.address;
    console.info("[Dev Faucet]: Player address -> ", address);
 
    const faucet = createFaucetService(networkConfig.faucetServiceUrl);
 
    const requestDrip = async () => {
      const balance = await publicClient.getBalance({ address });
      console.info(`[Dev Faucet]: Player balance -> ${balance}`);
      const lowBalance = balance < parseEther("1");
      if (lowBalance) {
        console.info("[Dev Faucet]: Balance is low, dripping funds to player");
        // Double drip
        await faucet.dripDev({ address });
        await faucet.dripDev({ address });
      }
    };
 
    requestDrip();
    // Request a drip every 20 seconds
    setInterval(requestDrip, 20000);
  }
 
  return {
    world,
    components,
    playerEntity: encodeEntity({ address: "address" }, { address: walletClient.account.address }),
    publicClient,
    walletClient,
    latestBlock$,
    blockStorageOperations$,
    waitForTransaction,
    worldContract,
    write$: write$.asObservable().pipe(share()),
  };
}
Explanation of the changes
import { createWalletClient, custom } from 'viem';
import { <network> } from 'viem/chains'

Import the Viem definitions necessary to use a wallet managed address (opens in a new tab).

  const walletClient = createWalletClient({
    chain: <network>,
    transport: custom(window.ethereum)
  })

Create the walletClient object.

const accounts = await walletClient.requestAddresses();

Wallets do not automatically give code on a web page access to the wallet information, not even addresses. Instead, you need to request access (opens in a new tab), and the user needs to approve it.

walletClient.account = { address: accounts[0] };

Because user approval is required, createWalletClient does not write the address into the return value. Therefore, we need to do it manually because MUD code expects the address to be in walletClient.account.address.

  return {
    ...
    playerEntity: encodeEntity({ address: "address" }, { address: walletClient.account.address }),
    ...
  };

We need to return the address of walletClient we created, not of the burner wallet we ignore.