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/Tables.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/Tables.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
    import { ClientComponents } from "./createClientComponents";
    import { SetupNetworkResult } from "./setupNetwork";
     
    export type SystemCalls = ReturnType<typeof createSystemCalls>;
     
    export function createSystemCalls(
      { worldSend, txReduced$, singletonEntity }: SetupNetworkResult,
      { Counter }: ClientComponents
    ) {
      const increment = async () => {
        const tx = await worldSend("increment", []);
        await awaitStreamValue(txReduced$, (txHash) => txHash === tx.hash);
        return getComponentValue(Counter, singletonEntity);
      };
     
      const decrement = async () => {
        const tx = await worldSend("decrement", []);
        await awaitStreamValue(txReduced$, (txHash) => txHash === tx.hash);
        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 worldSend("decrement", []);

    This is the way we call functions in top-level systems in a world. The second parameter, the list, is for the function parameters. In this case there aren't any, so it is empty.

    await awaitStreamValue(txReduced$, (txHash) => txHash === tx.hash);

    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 { mount as mountDevTools } from "@latticexyz/dev-tools";
    import { setup } from "./mud/setup";
     
    const {
      components,
      systemCalls: { decrement, increment },
    } = 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());
    };
     
    mountDevTools();
    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.