GitHub Repository

You can find the Adventure Layer Slither at the following link:
Adventure Layer ECS - Adventure Layer Slither.

The core modules of the Adventure Layer Slither are as follows.

1. Binding Independent Private Keys to Different Wallet Accounts

The Adventure Layer Slither allows binding independent private key accounts to different wallet accounts. Here’s how it works:

  • Private Key Generation and Storage:
    In mud/util.ts, the generatePrivateKey and privateKeyToAccount functions are used to generate a private key and convert it into account information. The private key can be stored in local storage for future use.

  • Private Key Validation:
    Also in mud/util.ts, the assertPrivateKey function is employed to validate the private key, ensuring it is in hexadecimal format and can successfully extract the corresponding address.

Relevant Code Snippet

import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { isHex, Hex } from "viem";

export const BurnerCacheKey = 'mud:burnerWallet';

function assertPrivateKey(privateKey: string, cacheKey: string): asserts privateKey is Hex {
  if (!isHex(privateKey)) {
    console.error("Private key found in cache is not valid hex", { privateKey, cacheKey });
    throw new Error(`Private key found in cache (${cacheKey}) is not valid hex`);
  }
  // Ensure the address can be extracted from the private key
  privateKeyToAccount(privateKey);
}

export function setBurnerPrivateKey(privateKey: string, cacheKey = "mud:burnerWallet"): Hex {
  localStorage.setItem(cacheKey, privateKey);
  
  if (privateKey === null) {
    console.error("Private key found in cache is not valid hex", { privateKey, cacheKey });
    throw new Error(`Private key found in cache (${cacheKey}) is not valid hex`);
  }

  assertPrivateKey(privateKey, cacheKey);
  console.log("Old burner wallet updated:", privateKeyToAccount(privateKey));
  
  return privateKey;
}

2. AGL Table Structure

In mud.config.ts, multiple AGL tables are defined, including:

  • Users
  • GameCodeToGameState
  • UserToOthersPositions
  • UserToOwnPositions
  • UserToSnakeDeque
  • Balance
  • UserAccountMapping

A total of 7 AGL tables are used to manage game data.

Code Snippet

tables: {
  Users: {
    schema: {
      player: "address",
      gameCode: "uint32",
      score: "uint32",
      username: "string",
    },
    key: ["player"],
  },
  GameCodeToGameState: {
    schema: {
      gameCode: "uint32",
      players: "address[]",
    },
    key: ["gameCode"],
  },
  UserToOthersPositions: {
    schema: {
      gameCode: "uint32",
      snakes: "bytes32[]",
    },
    key: ["gameCode"]
  },
  UserToOwnPositions: {
    schema: {
      player: "address",
      snake: "bytes32[]",
    },
    key: ["player"],
  },
  UserToSnakeDeque: {
    schema: {
      player: "address",
      snakeBody: "bytes32[]",
    },
    key: ["player"],
  },
  Balance: {
    schema: {
      player: "address",
      value: "uint256",
    },
    key: ["player"],
  },
  UserAccountMapping: {
    schema: {
      player: "address",
      account: "string",
    },
    key: ["player"],
  },
}

3. Main Contracts

IWorld.sol

This interface integrates all systems and associated function selectors that are dynamically registered in the World during deployment.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

/* Autogenerated file. Do not edit manually. */

import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";
import { IBalanceSystem } from "./IBalanceSystem.sol";
import { IGameStateSystem } from "./IGameStateSystem.sol";
import { IUserAccountSystem } from "./IUserAccountSystem.sol";
import { IUsersSystem } from "./IUsersSystem.sol";

/**
 * @title IWorld
 * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
 * @notice This interface integrates all systems and associated function selectors
 * that are dynamically registered in the World during deployment.
 * @dev This is an autogenerated file; do not edit manually.
 */
interface IWorld is IBaseWorld, IBalanceSystem, IGameStateSystem, IUserAccountSystem, IUsersSystem {}

BalanceSystem.sol

This contract handles balance-related operations for the game, including starting a game, querying balances, setting game fees, and managing player balances.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract BalanceSystem is System {
    address public owner;
    uint256 public gameFee = 0.01 ether; // Define game fee

    event GameStartedRecharge(address indexed player);
    event GamePlayedDeduct(address indexed player);

    constructor() {
        owner = msg.sender;
    }

    // Player pays and starts game
    function startGame() public payable {
        require(msg.value >= gameFee, "Insufficient funds to start the game");
        payable(owner).transfer(gameFee);
        rechargeBalance(msg.sender, gameFee);
        emit GameStartedRecharge(msg.sender);
    }

    // Query contract balance
    function getContractBalance() public view returns (uint256) {
        return address(this).balance;
    }

    // Query player contract balance
    function getPlayerBalance(address player) public view returns (uint256) {
        return Balance.get(player);
    }

    // Query current sender balance
    function getCurrentBalance() public view returns (uint256) {
        return Balance.get(msg.sender);
    }

    // Set game fee
    function setGameFee(uint256 newFee) public {
        require(msg.sender == owner, "Only owner can set game fee");
        gameFee = newFee;
    }

    // Deduct balance
    function deductBalance(address player, uint256 amount) public {
        uint256 currentBalance = Balance.get(player);
        require(currentBalance >= amount, "Insufficient balance");
        Balance.set(player, currentBalance - amount);
    }

    // Recharge balance
    function rechargeBalance(address player, uint256 amount) public {
        uint256 currentBalance = Balance.get(player);
        Balance.set(player, currentBalance + amount);
    }
}

UsersSystem.sol

This contract manages user-related operations, such as retrieving user data, starting a game, adding users, and generating random numbers.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { Users, UsersData, GameCodeToGameState } from "../codegen/index.sol";
import { System } from "@latticexyz/world/src/System.sol";

contract UsersSystem is System {
    function getData() public view returns (UsersData memory) {
        return Users.get(msg.sender);
    }

    function startGame(string memory name) public {
        uint32 gameCode = uint32(generateRandom() % 10000);
        Users.setGameCode(msg.sender, gameCode);
        Users.setUsername(msg.sender, name);
        addUser(gameCode);
    }

    function addUser(uint32 gameCode) public {
        if (gameStateExistsAndRemove(gameCode, false)) {
            GameCodeToGameState.pushPlayers(gameCode, msg.sender);
        }
    }

    function gameStateExistsAndRemove(uint32 gameCode, bool remove) public returns (bool) {
        address[] memory players = GameCodeToGameState.getPlayers(gameCode);
        for (uint256 i = 0; i < players.length; i++) {
            if (players[i] == msg.sender) {
                if (remove) {
                    GameCodeToGameState.updatePlayers(gameCode, uint256(i), GameCodeToGameState.getItemPlayers(gameCode, 0));
                    GameCodeToGameState.popPlayers(gameCode);
                }
                return false;
            }
        }
        return true;
    }

    function generateRandom() public view returns (uint256) {
        return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender)));
    }
}

These contracts work together to manage the game’s balance, state, and user data, providing a comprehensive system for game operations.

Here’s the translated document in English:

4.Heartbeat Mechanism Implementation

  1. Contract Task Management:

    • The ManageContractTask function is used to start or stop the polling tasks for the contract, managing these tasks through contractMap.
  2. Heartbeat Mechanism:

    • The startPolling function uses the ABI of the adventureHeatbeat function to package data and periodically send transactions via a timer.
    • The transaction sending is implemented by constructing, signing, and submitting the transaction.
    func (s *EthereumAPI) startPolling(ctx context.Context, task ContractTask) {
        key, err := crypto.HexToECDSA(task.PrivateKey)
        if err != nil {
            log.Error("Failed to parse private key: %v", err)
            return
        }
    
        fromAddr := crypto.PubkeyToAddress(key.PublicKey)
        nonce, err := s.b.GetPoolNonce(ctx, fromAddr)
        if err != nil {
            log.Error("Failed to get nonce:", err)
            return
        }
    
        contractABI, err := abi.JSON(strings.NewReader(`[{"inputs":[],"name":"adventureHeatbeat","outputs":[],"stateMutability":"nonpayable","type":"function"}]`))
        if err != nil {
            log.Error("Failed to parse contract ABI:", err)
            return
        }
        data, err := contractABI.Pack("adventureHeatbeat")
        if err != nil {
            log.Error("Failed to pack contract ABI:", err)
            return
        }
    
        for {
            select {
            case <-ctx.Done():
                log.Info("Polling stopped for contract:", task.Address.Hex())
                return
            default:
                gasLimit, err := s.estimateGas(ctx, &fromAddr, &task.Address, data)
                if err != nil {
                    log.Error("Failed to estimate gas:", err)
                    return
                }
                
                gasPrice, err := s.GasPrice(ctx)
                if err != nil {
                    log.Error("Failed to get gas price:", err)
                    return
                }
                
                task.sendTxMutex.Lock()
                tx := types.NewTx(&types.LegacyTx{
                    Nonce:    nonce,
                    To:       &task.Address,
                    Value:    big.NewInt(0),
                    Gas:      gasLimit,
                    GasPrice: big.NewInt(gasPrice.ToInt().Int64() * 2),
                    Data:     data,
                })
    
                signedTx, err := types.SignTx(tx, types.HomesteadSigner{}, key)
                if err != nil {
                    log.Error("Failed to sign transaction:", err)
                    task.sendTxMutex.Unlock()
                    return
                }
    
                transaction, err := SubmitTransaction(ctx, s.b, signedTx)
                task.sendTxMutex.Unlock()
                if err != nil {
                    log.Error("Failed to send transaction:", err)
                    return
                }
    
                log.Info("Transaction sent:", transaction.Hex())
                nonce++
    
                time.Sleep(task.Interval)
            }
        }
    }
    
  3. Concurrency Control:

    • The use of sync.Mutex ensures that the transaction sending is thread-safe.
  4. Periodic Invocation:

    • The periodic invocation is implemented through time.Sleep(task.Interval), where Interval is the time interval in milliseconds.

This mechanism ensures that the adventureHeatbeat function can be regularly called at specified time intervals, thus achieving the heartbeat function. Each call may trigger random refreshes of food in the game, among other operations.