Filters

# Create and Save a Game Properly

Make sure you have everything you need before proceeding:

In this section, you will:

  • Make use of the rules of checkers.
  • Update the message handler to create a game and return its ID.

In the previous section, you added the message to create a game along with its serialization and dedicated gRPC function with the help of Ignite CLI.

However, it does not create a game yet because you have not implemented the message handling. How would you do this?

# Some initial thoughts

Dwell on the following questions to guide you in the exercise:

  • How do you sanitize your inputs?
  • How do you avoid conflicts with past and future games?
  • How do you use your files that implement the checkers rules?

# Code needs

  • No Ignite CLI is involved here, it is just Go.
  • Of course, you need to know where to put your code - look for TODO.
  • How would you unit-test this message handling?
  • How would you use Ignite CLI to locally run a one-node blockchain and interact with it via the CLI to see what you get?

For now, do not bother with niceties like gas metering or event emission.

You must add code that:

  • Verifies input sanity.
  • Creates a brand new game.
  • Saves it in storage.
  • Returns the ID of the new game.

For input sanity, your code can only accept or reject a message. You cannot fix a message, as that would change its content and break the signature. However, remember that your application is called via ABCI's CheckTx for each transaction that it receives. It is at this point that your application can statelessly sanitize inputs. For each message type, Ignite CLI isolates this concern into a ValidateBasic function:

Copy func (msg *MsgCreateGame) ValidateBasic() error { _, err := sdk.AccAddressFromBech32(msg.Creator) if err != nil { return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err) } return nil } x checkers types message_create_game.go View source

It is in here that you can add further stateless checks on the message.

Ignite CLI isolated the create a new game concern into a separate file, x/checkers/keeper/msg_server_create_game.go, for you to edit:

Copy func (k msgServer) CreateGame(goCtx context.Context, msg *types.MsgCreateGame) (*types.MsgCreateGameResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) // TODO: Handling the message _ = ctx return &types.MsgCreateGameResponse{}, nil } x checkers keeper msg_server_create_game.go View source

Ignite CLI has conveniently created all the message processing code for you. You are only required to code the key features.

# Message verification coding steps

What is a well-formatted MsgCreateGame? Eventually, you want the black and red players to be able to play moves. They will send and sign transactions for that. So, at the very least, you can check that the addresses passed are valid:

Copy func (msg *MsgCreateGame) ValidateBasic() error { _, err := sdk.AccAddressFromBech32(msg.Creator) if err != nil { return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err) } + _, err = sdk.AccAddressFromBech32(msg.Black) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid black address (%s)", err) + } + _, err = sdk.AccAddressFromBech32(msg.Red) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid red address (%s)", err) + } return nil } x checkers types message_create_game.go View source

You should not try to check whether they have enough tokens to play as that would be a stateful check. Stateful checks are handled as part of the message handling behind ACBI's DeliverTx.

# Message handling coding steps

Given that you have already done a lot of preparatory work, what coding is involved? How do you replace // TODO: Handling the message?

  1. First, rules represents the ready-made file with the imported rules of the game:

    Copy import ( ... + "github.com/alice/checkers/x/checkers/rules" ... ) x checkers keeper msg_server_create_game.go View source
  2. Get the new game's ID with the Keeper.GetSystemInfo (opens new window) function created by the ignite scaffold single systemInfo... command:

    Copy systemInfo, found := k.Keeper.GetSystemInfo(ctx) if !found { panic("SystemInfo not found") } newIndex := strconv.FormatUint(systemInfo.NextId, 10) x checkers keeper msg_server_create_game.go View source

    You panic if you cannot find the SystemInfo object because there is no way to continue if it is not there. It is not like a user error, which would warrant returning an error.

  3. Create the object to be stored:

    Copy newGame := rules.New() storedGame := types.StoredGame{ Index: newIndex, Board: newGame.String(), Turn: rules.PieceStrings[newGame.Turn], Black: msg.Black, Red: msg.Red, } x checkers keeper msg_server_create_game.go View source

    Note the use of:

    • The rules.New() (opens new window) command, which is part of the checkers rules file you imported earlier.
    • The string content of the msg *types.MsgCreateGame, namely .Black and .Red.

    Also note that you lose the information about the creator. If your design is different, you may want to keep this information.

  4. Confirm that the values in the object are correct by checking the validity of the players' addresses:

    Copy err := storedGame.Validate() if err != nil { return nil, err } x checkers keeper msg_server_create_game.go View source

    .Red, and .Black need to be checked because they were copied as strings. You do not need to check .Creator because at this stage the message's signatures have been verified, and the creator is the signer.

    Note that by returning an error, instead of calling panic, players cannot stall your blockchain. They can still spam but at a cost, because they will still pay the gas fee up to this point.

  5. Save the StoredGame object using the Keeper.SetStoredGame (opens new window) function created by the ignite scaffold map storedGame... command:

    Copy k.Keeper.SetStoredGame(ctx, storedGame) x checkers keeper msg_server_create_game.go View source
  6. Prepare the ground for the next game using the Keeper.SetSystemInfo (opens new window) function created by Ignite CLI:

    Copy systemInfo.NextId++ k.Keeper.SetSystemInfo(ctx, systemInfo) x checkers keeper msg_server_create_game.go View source
  7. Return the newly created ID for reference:

    Copy return &types.MsgCreateGameResponse{ GameIndex: newIndex, }, nil x checkers keeper msg_server_create_game.go View source

You just handled the create game message by actually creating the game.

# Unit tests

To test your additions to the message's ValidateBasic, you can simply add cases to the existing message_create_game_test.go (opens new window). You can verify that your additions have made the existing test fail:

This should return:

Copy --- FAIL: TestMsgCreateGame_ValidateBasic (0.00s) --- FAIL: TestMsgCreateGame_ValidateBasic/valid_address (0.00s) message_create_game_test.go:37: Error Trace: /Users/alice/checkers/x/checkers/types/message_create_game_test.go:37 Error: Received unexpected error: github.com/alice/checkers/x/checkers/types.(*MsgCreateGame).ValidateBasic /Users/alice/checkers/x/checkers/types/message_create_game.go:50 github.com/alice/checkers/x/checkers/types.TestMsgCreateGame_ValidateBasic.func1 /Users/alice/checkers/x/checkers/types/message_create_game_test.go:32 invalid black address (empty address string is not allowed): invalid address Test: TestMsgCreateGame_ValidateBasic/valid_address

First, change the file's package to types_test for consistency:

Copy - package types + package types_test import( + "github.com/b9lab/checkers/x/checkers/types" ) x checkers types message_create_game_test.go View source

Then adjust the existing test cases and add to them:

Copy { - name: "invalid address", - msg: MsgCreateGame{ + name: "invalid creator address", + msg: types.MsgCreateGame{ Creator: "invalid_address", + Black: sample.AccAddress(), + Red: sample.AccAddress(), }, err: sdkerrors.ErrInvalidAddress, }, + { + name: "invalid black address", + msg: types.MsgCreateGame{ + Creator: sample.AccAddress(), + Black: "invalid_address", + Red: sample.AccAddress(), + }, + err: sdkerrors.ErrInvalidAddress, + }, + { + name: "invalid red address", + msg: types.MsgCreateGame{ + Creator: sample.AccAddress(), + Black: sample.AccAddress(), + Red: "invalid_address", + }, + err: sdkerrors.ErrInvalidAddress, + }, { - name: "valid address", - msg: MsgCreateGame{ + name: "valid addresses", + msg: types.MsgCreateGame{ Creator: sample.AccAddress(), + Black: sample.AccAddress(), + Red: sample.AccAddress(), }, }, x checkers types message_create_game_test.go View source

Your tests on /types should now pass.

Moving on to the keeper, try the unit test you prepared in the previous section again:

This should fail with:

Copy panic: SystemInfo not found [recovered] panic: SystemInfo not found ...

Your keeper was initialized with an empty genesis. You must fix that one way or another.

You can fix this by always initializing the keeper with the default genesis. However such a default initialization may not always be desirable. So it is better to keep this default initialization closest to the tests. Copy the setupMsgServer from msg_server_test.go (opens new window) into your msg_server_create_game_test.go. Modify it to also return the keeper:

Copy func setupMsgServerCreateGame(t testing.TB) (types.MsgServer, keeper.Keeper, context.Context) { k, ctx := keepertest.CheckersKeeper(t) checkers.InitGenesis(ctx, *k, *types.DefaultGenesis()) return keeper.NewMsgServerImpl(*k), *k, sdk.WrapSDKContext(ctx) } x checkers keeper msg_server_create_game_test.go View source

Note the new import:

Copy import ( "github.com/alice/checkers/x/checkers" ) x checkers keeper msg_server_create_game_test.go View source

Do not forget to replace setupMsgServer(t) with this new function everywhere in the file. For instance:

Copy msgServer, _, context := setupMsgServerCreateGame(t) x checkers keeper msg_server_create_game_test.go View source

Run the tests again with the same command as before:

The error has changed to Not equal, and you need to adjust the expected value as per the default genesis:

Copy require.EqualValues(t, types.MsgCreateGameResponse{ GameIndex: "1", }, *createResponse) x checkers keeper msg_server_create_game_test.go View source

One unit test is good, but you can add more, in particular testing whether the values in storage are as expected when you create a single game:

Copy func TestCreate1GameHasSaved(t *testing.T) { msgSrvr, keeper, context := setupMsgServerCreateGame(t) msgSrvr.CreateGame(context, &types.MsgCreateGame{ Creator: alice, Black: bob, Red: carol, }) systemInfo, found := keeper.GetSystemInfo(sdk.UnwrapSDKContext(context)) require.True(t, found) require.EqualValues(t, types.SystemInfo{ NextId: 2, }, systemInfo) game1, found1 := keeper.GetStoredGame(sdk.UnwrapSDKContext(context), "1") require.True(t, found1) require.EqualValues(t, types.StoredGame{ Index: "1", Board: "*b*b*b*b|b*b*b*b*|*b*b*b*b|********|********|r*r*r*r*|*r*r*r*r|r*r*r*r*", Turn: "b", Black: bob, Red: carol, }, game1) } x checkers keeper msg_server_create_game_test.go View source

Or when you create 3 (opens new window) games. Other tests could include whether the get all functionality works as expected after you have created 1 game (opens new window), or 3 (opens new window), or if you create a game in a hypothetical far future (opens new window). Also add games with badly formatted (opens new window) or missing input (opens new window).

# Interact via the CLI

Now you can also confirm that the transaction creates a game via the CLI. Start with:

Send your transaction as you did in the previous section under "Interact via the CLI":

A first good sign is that the output gas_used is slightly higher than it was before (gas_used: "52498"). After the transaction has been validated, confirm the current state.

Show the system info:

This returns:

Copy SystemInfo: nextId: "2"

List all stored games:

This returns a game at index 1 as expected:

Copy pagination: next_key: null total: "0" storedGame: - black: cosmos169mc8qqd6tlued00z23fs75tyecfcazpuwapc4 board: '*b*b*b*b|b*b*b*b*|*b*b*b*b|********|********|r*r*r*r*|*r*r*r*r|r*r*r*r*' index: "1" red: cosmos10mqyvj55hm4wunsd62wprwfv9ehcerkfghcjfl turn: b

Show the new game alone:

This returns:

Copy storedGame: black: cosmos169mc8qqd6tlued00z23fs75tyecfcazpuwapc4 board: '*b*b*b*b|b*b*b*b*|*b*b*b*b|********|********|r*r*r*r*|*r*r*r*r|r*r*r*r*' index: "1" red: cosmos10mqyvj55hm4wunsd62wprwfv9ehcerkfghcjfl turn: b

Now your game is in the blockchain's storage. Notice how alice was given the black pieces and it is already her turn to play. As a note for the next sections, this is how to understand the board:

Copy *b*b*b*b|b*b*b*b*|*b*b*b*b|********|********|r*r*r*r*|*r*r*r*r|r*r*r*r* ^X:1,Y:2 ^X:3,Y:6

Or if placed in a square:

Copy X 01234567 *b*b*b*b 0 b*b*b*b* 1 *b*b*b*b 2 ******** 3 ******** 4 r*r*r*r* 5 *r*r*r*r 6 r*r*r*r* 7 Y

You can also get this in a one-liner:

When you are done with this exercise you can stop Ignite's chain serve.

synopsis

To summarize, this section has explored:

  • How to add stateless checks on your message.
  • How to implement a Message Handler that will create a new game, save it in storage, and return its ID on receiving the appropriate prompt message.
  • How to create unit tests to demonstrate the validity of your code.
  • How to interact via the CLI to confirm that sending the appropriate transaction will successfully create a game.

# Overview of upcoming content

You will learn how to modify this handling in later sections by: