Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

07: add init game functionality #7

Merged
merged 7 commits into from
Oct 20, 2022
Merged

Conversation

thlorenz
Copy link
Owner

@thlorenz thlorenz commented Oct 20, 2022

previous | next


Summary

There is a lot in this PR so let's provide a summary first and then go into details. The
following changes were applied:

On the Rust Side

  • game account and related types created and annotated
  • init game instruction completed
  • utilities added to create the game account
  • asserts added to ensure passed accounts checkout
  • errors added in order to alert the user in case something is wrong

On the TypeScript Side

  • after updating the Rust code I ran yarn api:gen in order to get the SDK up to date
  • I then updated the tests to ensure all builds and works as expected

Workflow

The following workflow is what you can use in order to add features like this.

  1. update the Rust code to add a feature or fix an issue
  2. cargo build-bpf to update the .so file
  3. yarn api:gen to update the SDK
  4. yarn amman:start to restart amman with the updated .so file
  5. update tests and run them until they pass or you identify an issue with the program
  6. go back to 1.

Game Account

The game account holds state about the TicTacToe game, mainly the current board, who are the
players and who's turn is it.

#[derive(Debug, BorshSerialize, BorshDeserialize, ShankAccount)]
pub struct Game {
    pub player_x: Pubkey,
    pub player_o: Pubkey,
    pub board: [u8; 9],
    pub state: GameState,
    pub player_to_move: Player,
}

Note that it is annotated with ShankAccount in order to generate the proper SDK equivalent.

It uses custom types like GameState which will be included in the SDK as well because they
are annotated with BorshSerialize, BorshDeserialize. This is a convention that _ shank_ uses
in order to decide which types to include with the SDK.

Let's have a closer look at some of the methods of the generated TypeScript GameAccount,
focusing on those that you might use in your API or tests.

static fromAccountInfo()
static async fromAccountAddress()

Both are shortcuts to fetch and de/serialize the account from the blockchain. They are very
useful when you want to verify that an account was initialized/updated the way you expect.

static async getMinimumBalanceForRentExemption()

Allows you to fund an account properly.

pretty()

Is super useful to diagnose while developing as it returns a readable version of the account
that you can log to the console.

static gpaBuilder()

Provides a gpa builder that you can use to find accounts of this type on chain.

static fromArgs(args: GameArgs)

Creates a game account from the provided args in case and is used when the account is
deserialized from on-chain data.

Also notice that the file includes a gameBeet which is used to de/serialize the account.

Supporting Types

Notice that additionally to the account the SDK added types that are referenced by it and the
updated init game instruction.

> types
    GameState.ts
    index.ts
    InitializeGameArgs.ts
    Player.ts
    PlayerMove.ts

The Init Game Instruction

As part of the init game instruction asserts were added. Once those pass, it creates the Game PDA account, iniitializing the game such
that the player executing the transaction is set to be the first player playerX.

In order to verify that everything checks out we now require instruction args which we added to
the Rust definition.

As a result the create method in the SDK was updated as well when we run yarn api:gen.
Fortunately we immediately know which parts of the code we need to update due to the type
errors that result.

Screen Shot 2022-10-20 at 10 12 56 AM

Additionally the documentation of the generated method informs us what is missing, namely the
instrucion args.

Screen Shot 2022-10-20 at 10 14 27 AM

If we then follow definition into the generated code we can see exactly how we need to
provide them.

Screen Shot 2022-10-20 at 10 15 25 AM

After we updated the test code we can run it again and it should pass.

Debugging Issues

While I was preparing the above I actually made a mistake and my transaction failed.
Fortunately amman will run even failing transactions on the validator (skipping preflight),
such that they become inspectable in the amman explorer.

Note how the color of the transaction indicates that it failed.
Screen Shot 2022-10-20 at 10 19 02 AM

Scrolling down we can see the informative log which helped me triage the issue.

Screen Shot 2022-10-20 at 10 29 27 AM

That same information is logged by amman to the terminal.

Screen Shot 2022-10-20 at 10 29 48 AM

Finally after I fixed that issue I could verify in the explorer that my transaction completed properly.

Screen Shot 2022-10-20 at 10 48 16 AM

Screen Shot 2022-10-20 at 10 48 33 AM

Verifying Account State in Tests

However from looking at transaction in the explorer we couldn't verify that the game account state was properly set. We'll get to how to
make this visible in the explorer in the next step, but at any rate our tests should verify
this automatically.

To that purpose we use the Game.fromAccountAddress method which fetches and deserializes the game
account from our local validator. Then we use the spok assertion library to consisely
verify all properties on that account.

const gameAccount = await Game.fromAccountAddress(connection, gamePda)
spok(t, gameAccount, {
  $topic: 'game',
  state: GameState.WaitingForOpponent,
  playerX: spokSamePubkey(playerX),
  playerO: spokSamePubkey(PublicKey.default),
  board: spok.arrayElements(9),
})

What you can Do

Start amman via yarn amman:start and open the amman explorer to connect.

Then type yarn t to run your tests.

In order to see failing transactions change the following line in the test as follows:

   const args: InitializeGameInstructionArgs = {
-    initializeGameArgs: { game },
+    initializeGameArgs: { game: playerX },
   }

Since we're now sending the player account instead of the game account the checks fail and so
does the transaction. You can inspect the result in the amman explorer

You could also make changes to the Rust program, just remember to cargo build-bpf it and
restart amman via yarn amman:start every time you make a change.


previous | next

@thlorenz thlorenz merged commit 87c79fc into master Oct 20, 2022
@thlorenz thlorenz deleted the ix/init-game-functionality branch October 20, 2022 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant