# How to implement a Stackr MicroRollup: Building a Simon Game

By [0xrouss](https://paragraph.com/@vigara) · 2024-10-05

---

In this guide, we'll walk through the process of implementing a **Stackr** **Micro-Rollup** using the popular memory game **Simon** as our example. Stackr is a powerful framework for building scalable and efficient blockchain applications. We'll cover the key components of a Stackr Micro-Rollup and how they work together to create a functional decentralized application.

Table of Contents
-----------------

1.  Introduction to Stackr
    
    1.  Why use a Micro-Rollup?
        
2.  Setting Up the Project
    
    1.  Prerequisites
        
    2.  Setting Up your first Micro-Rollup
        
    3.  Project Structure
        
3.  Defining the State
    
4.  Creating the State Machine
    
5.  Implementing the Transitions
    
6.  Setting Up the Micro-Rollup
    
7.  Initializing the MRU
    
8.  Building the API Server
    
9.  Interacting with the MRU
    
    1.  Create a deterministic wallet
        
    2.  Create a game with the deterministic wallet
        
    3.  Submit user moves
        
10.  Conclusion
    

Introduction to Stackr
----------------------

Stackr is a framework that allows developers to build scalable blockchain applications using a rollup architecture. Micro-rollups (MRU) are scaling solutions that process executions **offchain** to facilitate complex logic and then submit proofs to the main chain, significantly **improving throughput** and **reducing costs**.

### Why use a Micro-Rollup?

Micro-rollups enhance blockchain scalability by executing complex logic offchain while ensuring **onchain verification** via an external layer called **Vulcan**. They are self-contained, stateful environments that can be hosted flexibly and operate efficiently without the burden of generating proofs. Acting as decentralized backends, MRUs combine web2's speed and user experience with blockchain's **security** and transparency, enabling developers to build scalable, secure apps with less complexity.

Setting Up the Project
----------------------

### Prerequisites

*   **Git**: You can download it [here](https://git-scm.com/book/ms/v2/Getting-Started-First-Time-Git-Setup)
    
*   **Bun**: Installation [guide](https://bun.sh/docs/installation)
    
*   **Stackr CLI**: The Stackr CLI helps you to create, register, and deploy your MRUs.
    
    Install it globally using:
    
        bun i -g @stackr/cli
        
    
    Run it without installing:
    
        bunx @stackr/cli
        
    

### Setting Up your first Micro-Rollup

Create a new project using the following command:

    bunx @stackr/cli@latest init
    

Choose the Empty template from the list and follow the instructions:

            _             _                        _ _
        ___| |_ __ _  ___| | ___ __            ___| (_)
       / __| __/ _` |/ __| |/ / '__|  _____   / __| | |
       \__ \ || (_| | (__|   <| |    |_____| | (__| | |
       |___/\__\__,_|\___|_|\_\_|             \___|_|_|
     
    ? Pick a template (Use arrow keys)
      Counter (Recommended)
      Chess
      Token Transfer
      Bridge
    ❯ Empty
    

Enter the project name you want and select the SQLite database.

    ? Database Driver (Use arrow keys)
    ❯ SQLite (Recommended)
      PostgreSQL
      MySQL
      MariaDB
    

After the CLI generates all the project you will have to follow the instructions it gives you:

    ✔ ⚙️  Set up MRU
    ✔ 🧹  Set up git
    ✔ ⬇️  Installed packages
     
    Get started by running the following commands:
     
      1. cd simon-game/
      2. cp .env.example .env & modify values
      3. Register your rollup using npx @stackr/cli@latest register
    

You can use the following .env values:

    PRIVATE_KEY= --YOUR DEVELOPMENT PRIVATE KEY--
    VULCAN_RPC=https://sepolia.vulcan.stf.xyz/
    L1_RPC=https://rpc2.sepolia.org/
    REGISTRY_CONTRACT=0x985bAfb3cAf7F4BD13a21290d1736b93D07A1760
    NODE_ENV=development
    DATABASE_URI=./db.sqlite
    

### Project Structure

Stackr has a default and recommended projects structure:

    ├── src
    │   ├── stackr
    │   │   ├── machine.ts
    │   │   ├── mru.ts
    │   │   ├── state.ts
    │   │   └── transitions.ts
    │   └── index.ts
    │── .env
    └── stackr.config.ts
    

The root directory contains the configuration files of your project.

*   `.env:` contains the necessary MRU requited environment variables
    
*   `stackr.config.ts`: exports the StackrConfig defining the MRU configuration, you can learn more about it [here](https://docs.stf.xyz/build/framework/config)
    

The src directory contains source files of your project.

*   `index.ts`: is the main entrypoint file containing the code to run the MRU and the Express API server to communicate with the MRU
    

The `src/stackr` directory contains the source code that defines the MRU with the Stackr SDK.

*   `machine.ts`: contains all the state machine configuration
    
*   `state.ts`: defines the custom state
    
*   `transitions.ts`: defines the state transition functions
    
*   `mru.ts`: exports the MicroRollup instance, the project starter doesn’t create it because is not necessary, but it will help us separate the logic
    

Defining the State
------------------

The first step in building a Stackr rollup is to define your application's state. In our Simon game example, we'll create a `SimonState` class that extends the `State` class from the Stackr SDK in the `state.ts` file.

    import { State } from "@stackr/sdk/machine";
    import { BytesLike, solidityPackedKeccak256 } from "ethers";
    
    // Define the structure of a Simon game
    interface Game {
        gameId: string;
        owner: string; // Connected wallet address
        user: string; // Deterministic generated wallet address
        startedAt: number;
        endedAt: number;
        roundCount: number;
        userSequence: string;
        gameSequence: string;
    }
    
    // Define the overall application state
    export interface AppState {
        games: Record<string, Game>;
    }
    
    // SimonState class extends the State class from @stackr/sdk
    export class SimonState extends State<AppState> {
        constructor(state: AppState) {
            super(state);
        }
    
        // Generate a root hash of the current state
        // This is used for state verification and integrity checks
        getRootHash(): BytesLike {
            return solidityPackedKeccak256(
                ["string"],
                [JSON.stringify(this.state.games)]
            );
        }
    }
    

This state definition includes a `games` object that stores all active Simon games, with each game containing properties like the game ID, owner, user, timestamps, and game sequences.

We store `owner` and `user` because in the frontend we are going to create a deterministic wallet with the connected wallet signature, doing this we are going to be able to play the Simon game without having to sign every transaction.

Creating the State Machine
--------------------------

Next, we'll create a state machine that uses our `SimonState` class in the `machine.ts` file. The state machine defines the initial state and links it to the transition functions.

    import { StateMachine } from "@stackr/sdk/machine";
    import genesisState from "../../genesis-state.json";
    import { SimonState } from "./state";
    import { transitions } from "./transitions";
    
    // Define the State Machine for the Simon game
    const machine = new StateMachine({
        id: "simon", // Unique identifier for this state machine
        stateClass: SimonState, // The state class used by this machine
        initialState: genesisState.state, // Initial state loaded from a JSON file
        on: transitions, // Transitions that can be applied to the state
    });
    
    export { machine };
    

Implementing Transitions
------------------------

Transitions are the heart of your Stackr rollup. They define how the state changes in response to user actions. In our Simon game, we'll implement two main transitions: `createGame` and `userMoves` in our `transitions.ts` file.

    import { Transitions, SolidityType } from "@stackr/sdk/machine";
    import { SimonState } from "./state";
    import { hashMessage } from "ethers";
    
    const colors: string[] = ["red", "blue", "yellow", "green"];
    
    // Define the createGame transition
    const createGame = SimonState.STF({
        schema: {
            timestamp: SolidityType.UINT,
            owner: SolidityType.ADDRESS,
        },
        handler: ({ state, inputs, msgSender, block, emit }) => {
            // Generate a unique game ID
            const gameId = hashMessage(
                `${msgSender}::${block.timestamp}::${
                    Object.keys(state.games).length
                }`
            );
    
            // Create a new game in the state
            state.games[gameId] = {
                gameId: gameId,
                owner: inputs.owner,
                user: msgSender,
                startedAt: block.timestamp,
                endedAt: 0,
                roundCount: 1,
                userSequence: "",
                gameSequence: generateNewMove(),
            };
    
            // Emit an event for game creation
            emit({
                name: "Game Created",
                value: gameId,
            });
    
            return state;
        },
    });
    
    // Helper function to generate a new move in the game sequence
    function generateNewMove(moves: string = "") {
        const randomIndex = Math.floor(Math.random() * colors.length);
        const newMove = colors[randomIndex];
        if (moves.length > 0) {
            return moves.concat(`,${newMove}`);
        }
        return moves.concat(`${newMove}`);
    }
    
    // Define the userMoves transition
    const userMoves = SimonState.STF({
        schema: {
            gameId: SolidityType.STRING,
            moves: SolidityType.STRING,
        },
        handler: ({ state, inputs, msgSender, block, emit }) => {
            const { gameId, moves } = inputs;
            const game = state.games[gameId];
    
            // Check if the user is valid
            if (game.user != msgSender) {
                emit({
                    name: "Invalid User",
                    value: `${gameId},${game.user},${msgSender}`,
                });
                return state;
            }
    
            game.userSequence = moves;
    
            // Check if the user's moves match the game sequence
            if (game.gameSequence == moves) {
                // If correct, generate a new move and increment the round
                game.gameSequence = generateNewMove(game.gameSequence);
                game.roundCount += 1;
    
                emit({
                    name: "New Move",
                    value: `${gameId},${game.roundCount}`,
                });
            } else {
                // If incorrect, end the game
                game.endedAt = block.timestamp;
    
                emit({
                    name: "Game Ended",
                    value: `${gameId},${game.roundCount}`,
                });
            }
    
            return state;
        },
    });
    
    // Export the transitions
    export const transitions: Transitions<SimonState> = {
        createGame,
        userMoves,
    };
    

These transitions handle creating new games and processing user moves, updating the state accordingly.

Setting Up the Micro-Rollup
---------------------------

The MRU is the core component that ties everything together. It's responsible for managing the state machine and processing actions.

    import { MicroRollup } from "@stackr/sdk";
    import { stackrConfig } from "../../stackr.config";
    import { machine } from "./machine";
    
    // Create a new MRU instance
    const mru = await MicroRollup({
        config: stackrConfig, // Configuration for the MRU
        stateMachines: [machine], // State machines used by the MRU
    });
    
    export { mru };
    

Initializing the MRU
--------------------

We need to initialize our MRU in the `index.ts` to start it, also we will initialize the playground for easier debugging and testing, give it a try you can play Dino there ;)

    import { ActionConfirmationStatus, MicroRollup } from "@stackr/sdk";
    import { machine } from "./stackr/machine.ts";
    import { Playground } from "@stackr/sdk/plugins";
    import express, { Request, Response } from "express";
    import { mru } from "./stackr/mru.ts";
    import path from "path";
    
    /**
     * Main function to set up and run the Stackr micro rollup server
     */
    const main = async () => {
        // Initialize the MRU instance
        await mru.init();
    
        // Initialize the Playground plugin for debugging and testing
        Playground.init(mru);
    

Building the API Server
-----------------------

We need a way to communite with the MRU without having it in the same application, we are going to use RESTful API Express server to send all the user interactions and receive all the state.

    import { ActionConfirmationStatus, MicroRollup } from "@stackr/sdk";
    import { machine } from "./stackr/machine.ts";
    import { Playground } from "@stackr/sdk/plugins";
    import express, { Request, Response } from "express";
    import { mru } from "./stackr/mru.ts";
    import path from "path";
    
    /**
     * Main function to set up and run the Stackr micro rollup server
     */
    const main = async () => {
        // Initialize the MRU instance
        await mru.init();
    
        // Initialize the Playground plugin for debugging and testing
        Playground.init(mru);
    
        // Set up Express server
        const app = express();
        app.use(express.json());
    
        // Serve static files from the "public" directory
        app.use(express.static(path.join(__dirname, "public")));
    
        // Enable CORS for all routes
        app.use((_req, res, next) => {
            res.header("Access-Control-Allow-Origin", "*");
            res.header(
                "Access-Control-Allow-Headers",
                "Origin, X-Requested-With, Content-Type, Accept"
            );
            next();
        });
    
        // Serve the HTML file for the Simon game
        app.get("/", (req: Request, res: Response) => {
            res.sendFile(path.join(__dirname, "public", "simon.html"));
        });
    
        /**
         * GET /info - Retrieve information about the MicroRollup instance
         * Returns domain information and schema map for action reducers
         */
        app.get("/info", (req: Request, res: Response) => {
            const schemas = mru.getStfSchemaMap();
            const { name, version, chainId, verifyingContract, salt } =
                mru.config.domain;
            res.send({
                signingInstructions: "signTypedDate(domain, schema.types, inputs)",
                domain: {
                    name,
                    version,
                    chainId,
                    verifyingContract,
                    salt,
                },
                schemas,
            });
        });
    
        /**
         * POST /:reducerName - Submit an action to a specific reducer
         * Processes the action and returns the result or any errors
         */
        app.post("/:reducerName", async (req: Request, res: Response) => {
            const { reducerName } = req.params;
    
            const actionReducer = mru.getStfSchemaMap()[reducerName];
    
            if (!actionReducer) {
                res.status(400).send({ message: "NO_REDUCER_FOR_ACTION" });
                return;
            }
    
            try {
                const { msgSender, signature, inputs } = req.body;
    
                const actionParams = {
                    name: reducerName,
                    inputs,
                    signature,
                    msgSender,
                };
    
                // Submit the action to the MicroRollup instance
                const ack = await mru.submitAction(actionParams);
                // Wait for the action to be confirmed (C1 status)
                const { errors, logs } = await ack.waitFor(
                    ActionConfirmationStatus.C1
                );
    
                if (errors?.length) {
                    throw new Error(errors[0].message);
                }
    
                res.status(201).send({ logs });
            } catch (e: any) {
                console.error("Error processing action:", e.message);
                res.status(400).send({ error: e.message });
            }
    
            return;
        });
    
        /**
         * GET /games - Retrieve all games from the state machine
         */
        app.get("/games", async (req: Request, res: Response) => {
            const { games } = machine.state;
            res.json(games);
        });
    
        /**
         * GET /games/:gameId - Retrieve a specific game by ID
         */
        app.get("/games/:gameId", async (req: Request, res: Response) => {
            const { gameId } = req.params;
            const { games } = machine.state;
    
            const game = games[gameId];
    
            if (!game) {
                res.status(404).send({ message: "GAME_NOT_FOUND" });
                return;
            }
    
            res.json(game);
        });
    
        // Start the server
        app.listen(3012, () => {
            console.log("Server running on port 3012");
        });
    };
    
    // Run the main function
    main();
    

This server provides endpoints for submitting actions, retrieving game information, and querying the current state of the rollup.

We also modified the main endpoint to serve a basic HTML file that is going to be the frontend of the Simon game, in the next section we are going to see how we can communicate between the frontend and the MRU.

Interacting with the MRU
------------------------

Let's see how we can communicate with the MRU in order to play the game through the API we have created.

### Create a deterministic wallet

We prompt the user to sign a message to generate a new wallet with that signature, we are going to create a game and send the user moves with this new wallet.

    // Function to create a deterministic wallet
    async function createDeterministicWallet() {
        await connectMetaMask();
        const signer = provider.getSigner();
        const msgSender = await signer.getAddress();
    
        const message = `Simon Game Wallet: ${msgSender}`;
        const signature = await signer.signMessage(message);
        const walletSeed = ethers.utils.keccak256(
            ethers.utils.toUtf8Bytes(signature)
        );
        deterministicWallet = new ethers.Wallet(walletSeed);
    
        console.log("Deterministic wallet address:", deterministicWallet.address);
    }
    

### Create a game with the deterministic wallet

We use the deterministic wallet to sign the transaction with the domain, schema types and the unique inputs like the timestamp and the owner address.

    // Event listener for "Create New Game" button click
    document
        .getElementById("createGameButton")
        .addEventListener("click", async () => {
            try {
                await createDeterministicWallet();
                const msgSender = deterministicWallet.address;
    
                // Define domain and schema types for signing data
                const domain = {
                    name: "Stackr MVP v0",
                    version: "1",
                    chainId: 11155111,
                    verifyingContract: "0x0000000000000000000000000000000000000000",
                    salt: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
                };
    
                const schemaTypes = {
                    CreateGame: [
                        { name: "timestamp", type: "uint256" },
                        { name: "owner", type: "address" },
                    ],
                    Action: [
                        { name: "name", type: "string" },
                        { name: "inputs", type: "CreateGame" },
                    ],
                };
    
                const createGameInputs = {
                    timestamp: Math.floor(Date.now() / 1000),
                    owner: await provider.getSigner().getAddress(),
                };
    
                const createGameAction = {
                    name: "createGame",
                    inputs: createGameInputs,
                };
    
                // Sign the data using the deterministic wallet
                const signature = await deterministicWallet._signTypedData(
                    domain,
                    schemaTypes,
                    createGameAction
                );
    
                // Send request to create a new game
                const response = await fetch("http://localhost:3012/createGame", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify({
                        msgSender,
                        inputs: createGameInputs,
                        signature,
                    }),
                });
    
                const result = await response.json();
                console.log("Game created with logs:", result.logs);
    
                // Store game ID and start the game
                gameId = result.logs[0].value;
                roundNumber = 1;
                startGame(gameId);
            } catch (error) {
                console.error("Error creating game:", error);
            }
        });
    

### Submit user moves

When the user play all the moves for that round we call this function to submit the round to the MRU using the API, if the logs return a `New Move` we continue playing but if it returns a `Game Ended` we stop the game and communicate it to the user.

    // Function to submit user's moves and proceed to the next round
    async function submitUserMoves() {
        try {
            const msgSender = deterministicWallet.address;
    
            // Define schema types for signing user's moves
            const schemaTypes = {
                UserMoves: [
                    { name: "gameId", type: "string" },
                    { name: "moves", type: "string" },
                ],
                Action: [
                    { name: "name", type: "string" },
                    { name: "inputs", type: "UserMoves" },
                ],
            };
    
            const userMoveInputs = {
                gameId,
                moves: userSequence,
            };
    
            const userMoveAction = {
                name: "userMoves",
                inputs: userMoveInputs,
            };
    
            const domain = {
                name: "Stackr MVP v0",
                version: "1",
                chainId: 11155111,
                verifyingContract: "0x0000000000000000000000000000000000000000",
                salt: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
            };
    
            // Sign user's moves using the deterministic wallet
            const signature = await deterministicWallet._signTypedData(
                domain,
                schemaTypes,
                userMoveAction
            );
    
            // Send request to submit user's moves
            const response = await fetch("http://localhost:3012/userMoves", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    msgSender,
                    inputs: userMoveInputs,
                    signature,
                }),
            });
    
            const result = await response.json();
            console.log("Move submitted with logs:", result.logs);
    
            // Handle game progression based on MRU response
            if (result.logs[0].name === "New Move") {
                userSequence = "";
                roundNumber++;
                displayGameSequence();
            } else if (result.logs[0].name === "Game Ended") {
                alert(
                    `Game Over! You reached round ${
                        result.logs[0].value.split(",")[1]
                    }`
                );
                document.getElementById("gameBoard").style.display = "none";
            }
        } catch (error) {
            console.error("Error submitting moves:", error);
        }
    }
    

You can find all the source code from the frontend in the `public/simon.html` file in the [repository](https://github.com/0xrouss/simon-game/blob/main/src/public/simon.html).You can play this Simon game by running `bun start` and access the URL [localhost:3012](http://localhost:3012/) to play your first Simon in an MRU

![](https://storage.googleapis.com/papyrus_images/6e88ed14bb3ca561b8f734f334eb321fe56e8f4e50940f8b1bd971252e5cb060.png)

Conclusion
----------

Implementing a Stackr rollup involves several key components working together:

*   Defining the application state
    
*   Creating a state machine
    
*   Implementing state transition functions
    
*   Setting up the Micro-Rollup (MRU)
    
*   Building an API server to interact with the rollup
    
*   Crafting the methods to communicate with the API
    

By following this structure, you can create scalable and efficient blockchain applications that leverage the power of rollups. The Simon game example demonstrates how to implement game logic, manage state, and process user actions within the Stackr framework.

Thank you for reading. If you have any questions or need further assistance, please feel free to contact me. You can connect with me on [X / Twitter](https://x.com/0xrouss) or [LinkedIn](https://www.linkedin.com/in/pablovigara/).

---

*Originally published on [0xrouss](https://paragraph.com/@vigara/how-to-implement-a-stackr-microrollup-building-a-simon-game)*
