Write less code when using Garnet. Automatically convert your solidity contracts to Go code!
Table of contents#
Open Table of contents
Problem with MUD#
One issue that I found while using the MUD lib is that I needed to write some game logic twice, once for the solidity contract and another for the optimistic rendering of the action.
When I created Garnet, I still had the same issue, I was writing the solidity contracts using MUD but then rewriting all the logic in GoLang to optimistically predict the result of the transaction execution.
Two problems araise with this issue:
- Time. You need to write the code in two different languages (and if need to change your contracts, you need to remember to change the code in 2 different places)
- Differences between languages. While coding in a different language you’ll use the language features to implement the code and the lines of code will not be one-to-one with the original solidity contract. A small difference in the logic between the contract and your new code will end up with all the predictions being invalid.
Solution#
I built a transpiler from Solidity to GoLang, the solution is currently focused on the Garnet lib.
The main blocker to create the transpiler was to get the AST (Abstract Syntax Tree) for all the solidity contracts. Luckily I found out that the MUD’s CLI package stores the AST for each contract compiled in the out
folder.
With that out of the way, I started to code the transpiler.
Garnet getters and setters#
Garnet is a MUD indexer and in-memory db. To get the blockchain information, you need to pass the table name and key.
The first challenge was to create simple functions to get the blockchain information without passing the table name because in solidity the getters are like this: TableName.get(key)
.
The second challenge was to parse the mud.config.ts
and identify all the enums, tables and variables. With all that information, I created all the respective structures in GoLang.
Lastly, I needed to create a way to generate predictions or MudEvents
. The events are going to be used when the TableName.set(key, value)
function is called in the solidity contract.
Predictions#
The real challenge started after creating the getters and setters: process all the AST code and generate the equivalent code in GoLang.
The way to do it was to read all the AST nodes and create code that works in GoLang for each node. For the table setters and getters I called the functions created in the previous step instead of transpiling the code one-to-one.
After one week of fighting the parser, regexs and string manipulation, I got a usable version. The code is not pretty but it works! You can check it out here
Code generated#
The tool always creates the files:
enums.go
. Where all the enums defined in the mud config file are defined.getters.go
. It produces three functions for each table, one to get an element by key, another one to get all the rows from a table and the last one to process the values from Garnet and return them with the correct type assigned, for example,uint64
, instead ofData.Field
setters.go
. For each table, it generates two functions, one to set an element by key and another to delete a row.types.go
. It’s just theGameObject
struct, this one has the reference to your Garnet instance.predictions.go
. This file joins bothgetters
andsetters
in a way that can be easily used by the transpiler. For example, instead of adding a new return value to all the functions witherror
, wepanic
and userecover
in the backend. It’s not the cleanest solution, but the current version is good enough for now.
For each MUD System, it will generate a new file. Here is a small example of how it works:
- Solidity
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
// Core
import {System} from "@latticexyz/world/src/System.sol";
import {Player} from "../codegen/tables/Player.sol";
import {Position} from "../codegen/tables/Position.sol";
import {addressToEntityKey} from "../addressToEntityKey.sol";
contract MoveSystem is System {
function Move(int32 newX, int32 newY) public {
bytes32 senderKey = addressToEntityKey(_msgSender());
// Make sure that the player is registered
require(Player.get(senderKey) == true, "wallet is not registered");
// Check that the X and Y are valid values
require(newX >= 0 && newY >= 0, "invalid X and Y");
Position.set(senderKey, newX, newY);
}
}
- Generated Go code
package garnethelpers
func (p *Prediction) Move(newX int64, newY int64, senderAddress string) {
senderKey := p.addressToEntityKey(senderAddress)
if !(p.PlayerGet(senderKey) == true) {
panic("wallet is not registered")
}
if !(newX >= int64(0) && newY >= int64(0)) {
panic("invalid X and Y")
}
p.PositionSet(senderKey, newX, newY)
}
NOTE: for small functions, it doesn’t save you much time, but you can see an example of a big function solidity function converted to GoLang code.
Example#
I used the tool to build Bochamon. For this project I didn’t write any predictions, everything was autogenerated by the garnetutils
.
Running the tool generates the folder garnethelpers
automatically:
garnetutils alpha -i ./superhack/backend/contracts-builder/contracts -o ./superhack/backend/internal/garnethelpers/
You can see the generated code here.
Using the generated code to predict the result of a Move
transaction is as simple as adding these lines:
prediction := garnethelpers.NewPrediction(b.db)
prediction.Move(int64(moveMsg.X), int64(moveMsg.Y), ws.WalletAddress)
You can add the prediction result to the database with these lines:
b.db.AddTxSent(data.UnconfirmedTransaction{
Txhash: txhash.Hex(),
Events: prediction.Events,
})