Filters

# Play With Cross-Chain Tokens

Make sure you have all you need before proceeding:

In this section, you will:

  • Discover the Inter-Blockchain Communication Protocol.
  • Accept wagers with tokens from other chains.
  • Refactor unit and integration tests.

When you introduced a wager you enabled players to play a game and bet on the outcome using the base staking token of your blockchain. What if your players want to play with other currencies? Your blockchain can represent a token from any other connected blockchain by using the Inter-Blockchain Communication Protocol (IBC).

Thus, you could expand the pool of your potential players by extending the pool of possible wager denominations via the use of IBC. How can you do this?

Your checkers application will be agnostic regarding tokens and relayers. Your only task is to enable the use of foreign tokens.

# Some initial thoughts

Before diving into the exercise, ask yourself:

  • What new information do you need?
  • How do you sanitize the inputs?
  • Are there new errors to report back?
  • What event should you emit?

# Code needs

When it comes to the code itself:

  • What Ignite CLI commands, if any, assist you?
  • How do you adjust what Ignite CLI created for you?
  • How would you unit-test these new elements?
  • 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?

# New information

Instead of defaulting to "stake", let players decide what string represents their token:

  1. Update the stored game:

    Copy message StoredGame { ... + string denom = 12; } proto checkers stored_game.proto View source
  2. Update the message to create a game:

    Copy message MsgCreateGame { ... + string denom = 5; } proto checkers tx.proto View source
  3. Instruct the Ignite CLI and Protobuf to recompile both files:

  4. It is recommended to also update the MsgCreateGame constructor:

    Copy - func NewMsgCreateGame(creator string, black string, red string, wager uint64) *MsgCreateGame { + func NewMsgCreateGame(creator string, black string, red string, wager uint64, denom string) *MsgCreateGame { return &MsgCreateGame{ ... + Denom: denom, } } x checkers types message_create_game.go View source
  5. Not to forget the CLI client:

    Copy func CmdCreateGame() *cobra.Command { cmd := &cobra.Command{ - Use: "create-game [black] [red] [wager]", + Use: "create-game [black] [red] [wager] [denom]", Short: "Broadcast message createGame", - Args: cobra.ExactArgs(3), + Args: cobra.ExactArgs(4), RunE: func(cmd *cobra.Command, args []string) (err error) { ... + argDenom := args[3] clientCtx, err := client.GetClientTxContext(cmd) ... msg := types.NewMsgCreateGame( ... + argDenom, ) ... }, } ... } x checkers ... cli tx_create_game.go View source
  6. This new field will be emitted during game creation, so add a new event key as a constant:

    Copy const ( ... + GameCreatedEventDenom = "denom" ) x checkers types keys.go View source

# Additional handling

The token denomination has been integrated into the relevant data structures. Now the proper denomination values need to be inserted in the right instances at the right locations:

  1. In the helper function to create the Coin in full_game.go:

    Copy func (storedGame *StoredGame) GetWagerCoin() (wager sdk.Coin) { - return sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(int64(storedGame.Wager))) + return sdk.NewCoin(storedGame.Denom, sdk.NewInt(int64(storedGame.Wager))) } x checkers types full_game.go View source
  2. In the handler that instantiates a game:

    Copy storedGame := types.StoredGame{ ... + Denom: msg.Denom, } x checkers keeper msg_server_create_game.go View source

    Also where it emits an event:

    Copy ctx.EventManager().EmitEvent( sdk.NewEvent(sdk.EventTypeMessage, ... + sdk.NewAttribute(types.GameCreatedEventDenom, msg.Denom), ) ) x checkers keeper msg_server_create_game.go View source

# Unit tests

The point of the tests is to make sure that the token denomination is correctly used. So you ought to add a denomination when creating a game (opens new window) and add it to all the stored games (opens new window) you check and all the emitted events (opens new window) you check. Choose a "stake" for all first games and something else for additional games, for instance "coin" (opens new window) and "gold" (opens new window) respectively.

Adjust your test helpers too:

  • The coins factory now needs to care about the denomination too:

    Copy - func coinsOf(amount uint64) sdk.Coins { + func coinsOf(amount uint64, denom string) sdk.Coins { return sdk.Coins{ sdk.Coin{ - Denom: sdk.DefaultBondDenom, + Denom: denom, Amount: sdk.NewInt(int64(amount)), }, } } x checkers testutil bank_escrow_helpers.go View source
  • To minimize the amount of work to redo, add an ExpectPayWithDenom helper, and have the earlier ExpectPay use it with the "stake" denomination:

    Copy func (escrow *MockBankEscrowKeeper) ExpectPay(context context.Context, who string, amount uint64) *gomock.Call { + return escrow.ExpectPayWithDenom(context, who, amount, sdk.DefaultBondDenom) + } + + func (escrow *MockBankEscrowKeeper) ExpectPayWithDenom(context context.Context, who string, amount uint64, denom string) *gomock.Call { whoAddr, err := sdk.AccAddressFromBech32(who) if err != nil { panic(err) } - return escrow.EXPECT().SendCoinsFromAccountToModule(sdk.UnwrapSDKContext(context), whoAddr, types.ModuleName, coinsOf(amount)) + return escrow.EXPECT().SendCoinsFromAccountToModule(sdk.UnwrapSDKContext(context), whoAddr, types.ModuleName, coinsOf(amount, denom)) } x checkers testutil bank_escrow_helpers.go View source

    Do the same with ExpectRefund (opens new window).

With the new helpers in, you can pepper call expectations with "coin" (opens new window) or "gold".

# Integration tests

You have fixed your unit tests. You need to do the same for your integration tests.

# Adjustments

You can also take this opportunity to expand the genesis state so that it includes a different coin.

  • Make sure your helper to make a balance cares about the denomination:

    Copy - func makeBalance(address string, balance int64) banktypes.Balance { + func makeBalance(address string, balance int64, denom string) banktypes.Balance { return banktypes.Balance{ Address: address, Coins: sdk.Coins{ sdk.Coin{ - Denom: sdk.DefaultBondDenom, + Denom: denom, Amount: sdk.NewInt(balance), }, }, } } tests integration ... keeper keeper_integration_suite_test.go View source
  • Since you want to add more coins, make a specific function to sum balances per denomination:

    Copy func addAll(balances []banktypes.Balance) sdk.Coins { total := sdk.NewCoins() for _, balance := range balances { total = total.Add(balance.Coins...) } return total } tests integration ... keeper keeper_integration_suite_test.go View source
  • In the bank genesis creation, add new balances:

    Copy func getBankGenesis() *banktypes.GenesisState { coins := []banktypes.Balance{ - makeBalance(alice, balAlice), - makeBalance(bob, balBob), - makeBalance(carol, balCarol), + makeBalance(alice, balAlice, "stake"), + makeBalance(bob, balBob, "stake"), + makeBalance(bob, balBob, "coin"), + makeBalance(carol, balCarol, "stake"), + makeBalance(carol, balCarol, "coin"), } supply := banktypes.Supply{ - Total: coins[0].Coins.Add(coins[1].Coins...).Add(coins[2].Coins...), + Total: addAll(coins), } ... } tests integration ... keeper keeper_integration_suite_test.go View source
  • Also adjust the helper that checks bank balances. Add a function to reduce the amount of refactoring:

    Copy func (suite *IntegrationTestSuite) RequireBankBalance(expected int, atAddress string) { + suite.RequireBankBalanceWithDenom(expected, "stake", atAddress) + } + + func (suite *IntegrationTestSuite) RequireBankBalanceWithDenom(expected int, denom string, atAddress string) { sdkAdd, err := sdk.AccAddressFromBech32(atAddress) suite.Require().Nil(err, "Failed to parse address: %s", atAddress) suite.Require().Equal( int64(expected), - suite.app.BankKeeper.GetBalance(suite.ctx, sdkAdd, sdk.DefaultBondDenom).Amount.Int64()) + suite.app.BankKeeper.GetBalance(suite.ctx, sdkAdd, denom).Amount.Int64()) } tests integration ... keeper keeper_integration_suite_test.go View source

# Additional test

With the helpers in place, you can add a test with three players playing two games with different tokens:

Copy func (suite *IntegrationTestSuite) TestPlayMoveToWinnerBankPaidDifferentTokens() { suite.setupSuiteWithOneGameForPlayMove() goCtx := sdk.WrapSDKContext(suite.ctx) suite.msgServer.CreateGame(goCtx, &types.MsgCreateGame{ Creator: alice, Black: bob, Red: carol, Wager: 46, Denom: "coin", }) suite.RequireBankBalance(balAlice, alice) suite.RequireBankBalanceWithDenom(0, "coin", alice) suite.RequireBankBalance(balBob, bob) suite.RequireBankBalanceWithDenom(balBob, "coin", bob) suite.RequireBankBalance(balCarol, carol) suite.RequireBankBalanceWithDenom(balCarol, "coin", carol) suite.RequireBankBalance(0, checkersModuleAddress) testutil.PlayAllMoves(suite.T(), suite.msgServer, sdk.WrapSDKContext(suite.ctx), "1", bob, carol, game1Moves) testutil.PlayAllMoves(suite.T(), suite.msgServer, sdk.WrapSDKContext(suite.ctx), "2", bob, carol,game1Moves) suite.RequireBankBalance(balAlice, alice) suite.RequireBankBalanceWithDenom(0, "coin", alice) suite.RequireBankBalance(balBob+45, bob) suite.RequireBankBalanceWithDenom(balBob+46, "coin", bob) suite.RequireBankBalance(balCarol-45, carol) suite.RequireBankBalanceWithDenom(balCarol-46, "coin", carol) suite.RequireBankBalance(0, checkersModuleAddress) suite.RequireBankBalanceWithDenom(0, "coin", checkersModuleAddress) } tests integration ... keeper msg_server_play_move_test.go View source

All your tests should now pass.

# Interact via the CLI

Restart Ignite with chain serve. If you recall, Alice's and Bob's balances have two token denominations. Query:

This returns what you would expect from the config.yml (opens new window):

Copy balances: - amount: "100000000" denom: stake - amount: "20000" denom: token pagination: next_key: null total: "0"

You can make use of this other token to create a new game that costs 1 token:

Which mentions:

Copy ... - key: wager value: "1" - key: denom value: token ...

Have Alice play once:

Which mentions:

Copy - attributes: - key: recipient value: cosmos16xx0e457hm8mywdhxtmrar9t09z0mqt9x7srm3 - key: sender value: cosmos180g0kaxzzre95f9gww93t8cqhshjydazu7g35n - key: amount value: 1token type: transfer

This seems to indicate that Alice has been charged the wager. As a side node, cosmos16xx0e457hm8mywdhxtmrar9t09z0mqt9x7srm3 is the checkers module's address. Confirm it:

This returns:

Copy balances: - amount: "100000000" denom: stake - amount: "19999" denom: token pagination: next_key: null total: "0"

Correct. You made it possible to wager any token. That includes IBC tokens.

Now check the checkers module's balance:

This prints:

Copy balances: - amount: "1" denom: token pagination: next_key: null total: "0"

That is correct.

synopsis

To summarize, this section has explored:

  • How to enable the use of cross-chain tokens to make wagers on checkers games as well as your blockchain's base staking token, by making use of the Inter-Blockchain Communication Protocol (IBC).
  • How to update the stored game and the game creation message to allow players to decide what string represents their token.
  • Where to insert the necessary values to allow recognition of token denominations.
  • How to fix your existing tests due to the introduction of a new field and a new event, and how to add a new test when a player makes their first move.
  • How to interact via the CLI to confirm the presence of the new token denomination in a player's balance and that using these tokens to make a wager functions as required.
  • How to demonstrate that your application will accept IBC-foreign tokens from another blockchain, using Ignite CLI's built-in TypeScript relayer as a convenient small-scale local testing tool.

Alternatively, you can learn how to create the TypeScript client elements for your blockchain.