Add a system

In this tutorial you add a system to decrement the counter and update the application to use it.

Add a contract for the new system

Create a file packages/contracts/src/systems/DecrementSystem.sol. Note that we could have just added a function to the existing system, IncrementSystem.sol. The only reason we are adding a new system here is to see how to do it.

DecrementSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
 
import { System } from "@latticexyz/world/src/System.sol";
import { Counter } from "../codegen/index.sol";
 
contract DecrementSystem is System {
  function decrement() public returns (uint32) {
    uint32 counter = Counter.get();
    uint32 newValue = counter - 1;
    Counter.set(newValue);
    return newValue;
  }
}
Explanation

import { System } from "@latticexyz/world/src/System.sol";
import { Counter } from "../codegen/index.sol";

The two things the system needs to know: how to be a System and how to access the Counter.

uint32 counter = Counter.get();

Get the counter value.

Counter.set(newValue);

Set the counter to a new value.

Add decrement to the application

Having a system be able to do something doesn't help anybody unless it is called from somewhere. In this case, the vanilla getting started front end.

  1. Edit packages/client/src/mud/createSystemCalls.ts to include decrement. This is the file after the changes:

    createSystemCalls.ts
    /*
    * 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";
    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/vanilla/packages/client/src/mu$
      *   - waitForTransaction (which comes from syncToRecs, see
      *     https://github.com/latticexyz/mud/blob/26dabb34321eedff7a43f3fcb46da4f3f5ba3708/templates/vanilla/packages/client/src/mu$
      *
      * - 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/vanilla/packages/client/src/mud/setupN$
      */
      { 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);
      };
     
      const decrement = async () => {
        const tx = await worldContract.write.decrement();
        await waitForTransaction(tx);
        return getComponentValue(Counter, singletonEntity);
      };
     
      return {
        increment,
        decrement,
      };
    }
    Explanation

    The new function is decrement.

    const decrement = async () => {

    This function involves sending a transaction, which is a slow process, so it needs to be asynchronous (opens in a new tab).

      const tx = await worldContract.write.decrement();

    This is the way we call functions in top-level systems in a world.

      await waitForTransaction(tx);

    Await until we receive confirmation that the transaction has been added to a block.

        return getComponentValue(Counter, singletonEntity)
    };

    Get the value of Counter to return it. It should already be the updated value.

    return {
      increment,
      decrement,
    };

    Of course, we also need to return decrement so it can be used elsewhere.

  2. Update packages/client/src/index.ts to include decrement. This is the file after the changes:

    index.ts
    import { setup } from "./mud/setup";
    import mudConfig from "contracts/mud.config";
     
    const {
      components,
      systemCalls: { increment, decrement },
      network,
    } = await setup();
     
    // Components expose a stream that triggers when the component is updated.
    components.Counter.update$.subscribe((update) => {
      const [nextValue, prevValue] = update.value;
      console.log("Counter updated", update, { nextValue, prevValue });
      document.getElementById("counter")!.innerHTML = String(nextValue?.value ?? "unset");
    });
     
    // Just for demonstration purposes: we create a global function that can be
    // called to invoke the Increment system contract via the world. (See IncrementSystem.sol.)
    (window as any).increment = async () => {
      console.log("new counter value:", await increment());
    };
     
    (window as any).decrement = async () => {
      console.log("new counter value:", await decrement());
    };
     
    // 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: network.publicClient,
        walletClient: network.walletClient,
        latestBlock$: network.latestBlock$,
        storedBlockLogs$: network.storedBlockLogs$,
        worldAddress: network.worldContract.address,
        worldAbi: network.worldContract.abi,
        write$: network.write$,
        recsWorld: network.world,
      });
    }
    Explanation

    const {
      components,
      systemCalls: { decrement, increment },
    } = await setup();

    This syntax means the we call setup() (opens in a new tab), and then set components, systemCalls.increment, and systemCalls.decrement to the values provided in the hash returned by this function. systemCalls comes from createSystemCalls(), which we modified in the previous step.

    (window as any).decrement = async () => {
      console.log("new counter value:", await decrement());
    };

    We need to make decrement available to our application code. Most frameworks have a standard mechanism to do this, but we are using vanilla, which doesn't - so we add it to window which is a global variable.

  3. Modify packages/client/index.html to add a decrement button. This is the file after the changes:

    index.html
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>a minimal MUD client</title>
      </head>
      <body>
        <script type="module" src="/src/index.ts"></script>
        <div>Counter: <span id="counter">0</span></div>
        <button onclick="window.increment()">Increment</button>
        <button onclick="window.decrement()">Decrement</button>
      </body>
    </html>
    Explanation

    <button onclick="window.decrement()">Decrement</button>

    Create a button (opens in a new tab) with an onClick (opens in a new tab) property.

  4. Reload the application to see that there is a decrement button and that you can use it.