# Handle Wager Payments
Make sure you have everything you need before proceeding:
- You understand the concepts of modules and keepers.
- Go is installed.
- You have the checkers blockchain codebase up to the game wager. If not, follow the previous steps or check out the relevant version (opens new window).
In this section, you will:
- Work with the bank module.
- Handle money.
- Use mocks.
- Add integration tests.
In the previous section, you introduced a wager. On its own, having a Wager
field is just a piece of information, it does not transfer tokens just by existing.
Transferring tokens is what this section is about.
# Some initial thoughts
When thinking about implementing a wager on games, ask:
- Is there any desirable atomicity of actions?
- At what junctures do you need to handle payments, refunds, and wins?
- Are there errors to report back?
- What event should you emit?
In the case of this example, you can consider that:
- Although a game creator can decide on a wager, it should only be the holder of the tokens that can decide when they are being taken from their balance.
- You might think of adding a new message type, one that indicates that a player puts its wager in escrow. On the other hand, you can leverage the existing messages and consider that when a player makes their first move, this expresses a willingness to participate, and therefore the tokens can be transferred at this juncture.
- For wins and losses, it is easy to imagine that the code handles the payout at the time a game is resolved.
# Code needs
When it comes to your code:
- What Ignite CLI commands, if any, will assist you?
- How do you adjust what Ignite CLI created for you?
- Where do you make your changes?
- How would you unit-test these new elements?
- Are unit tests sufficient here?
- 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?
Here are some elements of response:
- Your module needs to call the bank to tell it to move tokens.
- Your module needs to be allowed by the bank to keep tokens in escrow.
- How would you test your module when it has such dependencies on the bank?
# What is to be done
A lot is to be done. In order you will:
- Make it possible for your checkers module to call certain functions of the bank to transfer tokens.
- Tell the bank to allow your checkers module to hold tokens in escrow.
- Create helper functions that encapsulate some knowledge about when and how to transfer tokens.
- Use these helper functions at the right places in your code.
- Update your unit tests and make use of mocks for that. You will create the mocks, create helper functions and use all that.
- Prepare your code to accept integration tests.
- Create helper functions that will make your integration tests more succinct.
- Add integration tests that create a full app and test proper token bank balances.
# Declaring expectations
On its own the Wager
field does not make players pay the wager or receive rewards. You need to add handling actions that ask the bank
module to perform the required token transfers. For that, your keeper needs to ask for a bank
instance during setup.
The only way to have access to a capability with the object-capability model of the Cosmos SDK is to be given the reference to an instance which already has this capability.
Payment handling is implemented by having your keeper hold wagers in escrow while the game is being played. The bank
module has functions to transfer tokens from any account to your module and vice-versa.
Alternatively, your keeper could burn tokens instead of keeping them in escrow and mint them again when paying out. However, this makes your blockchain's total supply falsely fluctuate. Additionally, this burning and minting may prove questionable when you later introduce IBC tokens.
Declare an interface that narrowly declares the functions from other modules that you expect for your module. The conventional file for these declarations is x/checkers/types/expected_keepers.go
.
The bank
module has many capabilities, but all you need here are two functions. Your module already expects one function of the bank keeper: SpendableCoins
(opens new window). Instead of expanding this interface, you add a new one and redeclare the extra functions you need like so:
These two functions must exactly match the functions declared in the bank
's keeper.go file (opens new window). Copy the declarations directly from the bank
's file. In Go, any object with these two functions is a BankEscrowKeeper
.
# Obtaining the capability
With your requirements declared, it is time to make sure your keeper receives a reference to a bank keeper. First add a BankEscrowKeeper
to your keeper in x/checkers/keeper/keeper.go
:
This BankEscrowKeeper
is your newly declared narrow interface. Do not forget to adjust the constructor accordingly:
Next, update where the constructor is called and pass a proper instance of BankKeeper
. This happens in app/app.go
:
This app.BankKeeper
is a full bank
keeper that also conforms to your BankEscrowKeeper
interface.
Finally, inform the app that your checkers module is going to hold balances in escrow by adding it to the whitelist of permitted modules:
If you compare it to the other maccperms
lines, the new line does not mention any authtypes.Minter
or authtypes.Burner
. Indeed nil
is what you need to keep in escrow. For your information, the bank creates an address for your module's escrow account. When you have the full app
, you can access it with:
# Preparing expected errors
There are several new error situations that you can enumerate with new variables:
# Money handling steps
With the bank
now in your keeper, it is time to have your keeper handle the money. Keep this concern in its own file, as the functions are reused on play, reject, and forfeit.
Create the new file x/checkers/keeper/wager_handler.go
and add three functions to collect a wager, refund a wager, and pay winnings:
The Must
prefix in the function means that the transaction either takes place or a panic
is issued. If a player cannot pay the wager, it is a user-side error and the user must be informed of a failed transaction. If the module cannot pay, it means the escrow account has failed. This latter error is much more serious: an invariant may have been violated and the whole application must be terminated.
Now set up collecting a wager, paying winnings, and refunding a wager:
Collecting wagers happens on a player's first move. Therefore, differentiate between players:
When there are no moves, get the address for the black player:
Try to transfer into the escrow:
Then do the same for the red player (opens new window) when there is a single move.
Paying winnings takes place when the game has a declared winner. First get the winner. "No winner" is not an acceptable situation in this
MustPayWinnings
. The caller of the function must ensure there is a winner:Then calculate the winnings to pay:
You double the wager only if the red player has also played and therefore both players have paid their wagers.
If you did this wrongly, you could end up in a situation where a game with a single move pays out as if both players had played. This would be a serious bug that an attacker could exploit to drain your module's escrow fund.
Then pay the winner:
Finally, refunding wagers takes place when the game has partially started, i.e. only one party has paid, or when the game ends in a draw. In this narrow case of
MustRefundWager
:Refund the black player when there has been a single move:
If the module cannot pay, then there is a panic as the escrow has failed.
You will notice that no special case is made when the wager is zero. This is a design choice here, and which way you choose to go is up to you. Not contacting the bank unnecessarily is cheaper in gas. On the other hand, why not outsource the zero check to the bank?
# Insert wager handling
With the desired steps defined in the wager handling functions, it is time to invoke them at the right places in the message handlers.
When a player plays for the first time:
When a player wins as a result of a move:
When a player rejects a game:
When a game expires and there is a forfeit, make sure to only refund or pay full winnings when applicable. The logic needs to be adjusted:
# Unit tests
If you try running your existing tests you get a compilation error on the test keeper builder (opens new window). Passing nil
would not get you far with the tests and creating a full-fledged bank keeper would be a lot of work and not a unit test. See the integration tests below for that.
Instead, you create mocks and use them in unit tests, not only to get the existing tests to pass but also to verify that the bank is called as expected.
# Prepare mocks
It is better to create some mocks (opens new window). The Cosmos SDK does not offer mocks of its objects so you have to create your own. For that, the gomock
(opens new window) library is a good resource. Install it:
With the library installed, you still need to do a one time creation of the mocks. Run:
If your expected keepers change, you will have to run this command again. It can be a good idea to save the command for future reference. You may use a Makefile
for that. Ensure you install the make
tool for your computer. If you use Docker, add it to the packages and rebuild the image:
Create the Makefile
:
At any time, you can rebuild the mocks with:
You are going to set the expectations on this BankEscrowKeeper
mock many times, including when you do not care about the result. So instead of mindlessly setting the expectations in every test, it is in your interest to create helper functions that will make setting up the expectations more efficient. Create a new bank_escrow_helpers.go
file with:
# Make use of mocks
With the helpers in place, you can add a new function similar to CheckersKeeper(t testing.TB)
but which uses mocks. Keep the original function, which passes a nil
for bank.
The CheckersKeeperWithMocks
function takes the mock in its arguments for more versatility.
Now adjust the small functions that set up the keeper before each test. You do not need to change them for the create tests because they never call the bank. You have to do it for play, reject, and forfeit.
For play:
This function creates the mock and returns two new objects:
- The mock controller, so that the
.Finish()
method can be called within the test itself. This is the function that will verify the call expectations placed on the mocks. - The mocked bank escrow. This is the instance on which you place the call expectations.
Both objects will be used from the tests proper.
Do the same for reject (opens new window). If your forfeit unit tests do not use setupMsgServerWithOneGameForPlayMove
, then you should also create one such function the forfeit tests.
# Adjust the unit tests
With these changes, you need to adjust many unit tests for play, reject, and forfeit. For many, you may only want to make the tests pass again without checking any meaningful bank call expectations. There are different situations:
The mocked bank is not called. So you do not add any expectation, and still call the controller:
The mocked bank is called, but you do not care about how it was called:
The mocked bank is called, and you want to add call expectations:
Go ahead and make the many necessary changes as you see fit.
# Wager handler unit tests
After these adjustments, it is a good idea to add unit tests directly on the wager handling functions of the keeper. Create a new wager_handler_test.go
file. In it:
Add a setup helper function that does not create any message server:
Add tests on the
CollectWager
function. For instance, when the game is malformed:Or when the black player failed to escrow the wager:
Or when the collection of a wager works:
Add similar tests to the payment of winnings from the escrow. When it fails:
Or when it works:
You will also need a test for refund (opens new window) situations.
# Add bank escrow unit tests
Now that the wager handling has been convincingly tested, you want to confirm that its functions are called at the right junctures. Add dedicated tests with message servers that confirm how the bank is called. Add them in existing files, for instance:
After doing all that, confirm that your tests run.
# Integration tests
Your unit tests pass, and they confirm that the bank is called as per your expectations. It would be nice to add further tests that use a real bank. This is possible with the help of integration tests.
As a reminder:
- At version 0.45.4 of the Cosmos SDK, an integration test creates a full app.
- At version 0.47 of the SDK, an integration test creates a minimal app, and a test that creates a full app is called an end-to-end test (E2E).
Fortunately, you do not have to do this from scratch: taking inspiration from tests on the bank module (opens new window), prepare your code so as to accommodate and create a full app that will contain a bank keeper, and add new tests.
For unit tests, each function takes a t *testing.T
(opens new window) object. For integration tests, each function will be a method on a test suite that inherits from testify's suite (opens new window). This has the advantage that your test suite can have as many fields as is necessary or useful. The objects that you have used and would welcome in the suite are:
You can spread the suite's methods to different files, so as to keep consistent naming for your test files.
When testing, go test
will find the suite because you add a regular test (opens new window) that initializes the suite and runs it. The test suite is then automatically initialized with its SetupTest
(opens new window) function via its parent suite
class. After that, all the methods of the test suite are run.
# Accommodate your code
Copy and adjust from the Cosmos SDK.
Ignite CLI created a default constructor for your App with a cosmoscmd.App
(opens new window) return type, but this is not convenient. Instead of risking breaking other dependencies, add a new constructor with your App
(opens new window) as the return type.
Use encoding.go
(opens new window) taken from here (opens new window), where you:
- Import
"github.com/ignite-hq/cli/ignite/pkg/cosmoscmd"
. - Replace
simappparams.EncodingConfig
withcosmoscmd.EncodingConfig
. - Replace
simappparams.MakeTestEncodingConfig
withappparams.MakeTestEncodingConfig
.
Use proto.go
(opens new window) taken from here (opens new window), where you:
- Import
"github.com/ignite-hq/cli/ignite/pkg/cosmoscmd"
. - Replace
EncodingConfig
withcosmoscmd.EncodingConfig
.
Use test_helpers.go
(opens new window) taken from here (opens new window), in which you:
Adjust from
SimApp
toApp
Adjust from
New()
toNewApp()
Initialize your checkers genesis:
Define your test suite in a new keeper_integration_suite_test.go
file in a dedicated folder tests/integration/checkers/keeper
:
Direct go test
to it:
Create the suite.SetupTest
function, taking inspiration from the bank tests (opens new window):
This SetupTest
function (opens new window) is like a beforeEach
as found in other test libraries. With it, you always get a new app
in each test, without interference between them. Do not omit it (opens new window) unless you have specific reasons to do so.
It collects your checkersModuleAddress
for later use in tests that check events and balances:
# Test the test suite
You can now confirm you did all this correctly by running these new keeper integration tests, although the suite has no tests. Note how the path to call has changed:
# Helpers for money checking
Your upcoming integration tests will include checks on wagers being paid, lost, and won, so your tests need to initialize some bank balances for your players. This is made easier with a few helpers, including a helper to confirm a bank balance.
Make a bank genesis
Balance
(opens new window) type from primitives:Declare default accounts and balances that will be useful for you:
Make your preferred bank genesis state:
Add a simple function to prepare your suite with your desired balances:
Add a function to check balances from primitives:
With the preparation done, what does an integration test method look like?
# Anatomy of an integration suite test
Now you must add integration tests for your keeper in new files. What does an integration test look like? Take the example of a simple unit test (opens new window) ported to the integration test suite:
The method has a declaration:
It is declared as a member of your test suite, and is prefixed with
Test
(opens new window).The setup can be done as you like, but just like for unit tests you ought to create a helper and use it. Here one exists already:
The action is no different from a unit test's action, other than that you get the
keeper
ormsgServer
from the suite's fields:The verification is done with
suite.Require().X
, but otherwise looks similar to the shorterrequire.X
of unit tests:In fact, it is exactly the same
require
(opens new window) object.
You have added an integration test that copies an existing unit test. It demonstrates the concept but is of limited additional utility.
# Extra tests
It is time to add extra tests that check money handling by the bank. Before jumping in, as you did in play unit tests you can add a method that prepares your suite's keeper with a game ready to be played on:
You will call this game from the relevant tests. You can do the same for reject (opens new window).
For the tests proper, before an action that you expect to transfer money (or not) you can verify the initial position:
After the action you can test the new balances, for instance:
How you subdivide your tests and where you insert these balance checks is up to you. You can find examples here for:
- Creating a game (opens new window).
- Playing the first move (opens new window), the second move (opens new window), including up to a resolution (opens new window). You can also check the events (opens new window).
- Failing to play a game because of a failure to pay the wager on the first move (opens new window) and the second move (opens new window).
- Rejecting a game (opens new window), including when there have been moves played (opens new window).
- Forfeiting a game (opens new window), including when there has been one move played (opens new window) or two (opens new window).
# What happened to the events?
With the new tests, you may think that the events are compromised. For instance, the event type "transfer"
normally comes with three attributes, but when the bank has made two transfers the "transfer"
event ends up with 6 attributes. This is just the way events are organized: per type, with the attributes piled in.
When checking emitted events, you need to skip over the attributes you are not checking. You can easily achieve that with Go slices.
For instance, here transferEvent.Attributes[6:]
discards the first six attributes:
# Debug your suite
You learned in a previous section how to launch a test in debug mode. It is still possible to do so when using a suite. Depending on the versions of your Go installation and your Visual Studio Code, you can launch it in two ways:
Right-click on the arrow to the left of the suite's runner
func TestCheckersKeeperTestSuite
:In this case, you can only launch debug for all of the suite's test methods and not just a single one (as is possible with a simple test).
Right-click on the arrow to the left of the separate test of the suite:
This option may not be available. If being able to debug only a few tests at a time is important to you, a solution is to create more granular suites, for example using one or more test suites per file and falling back on the first option.
# Interact via the CLI
With the tests done, see what happens at the command-line.
Keep the game expiry at 5 minutes to be able to test a forfeit, as done in a previous section. Now, you need to check balances after relevant steps to test that wagers are being withheld and paid.
How much do Alice and Bob have to start with?
This prints:
# A game that expires
Create a game on which the wager will be refunded because the player playing red
did not join:
Confirm that the balances of both Alice and Bob are unchanged - as they have not played yet.
In this example, Alice paid no gas fees, other than the transaction costs, to create a game. The gas price is likely 0
here anyway. This is fixed in the next section.
Have Alice play:
Confirm that Alice has paid her wager:
This prints:
Wait 5 minutes for the game to expire and check again:
This prints:
# A game played twice
Now create a game in which both players only play once each, i.e. where the player playing black
forfeits:
Confirm that both Alice and Bob paid their wagers. Wait 5 minutes for the game to expire and check again:
This shows:
This is correct: Bob was the winner by forfeit.
Similarly, you can test that Alice gets her wager back when Alice creates a game, Alice plays, and then Bob rejects it.
It would be difficult to test by CLI when there is a winner after a full game. That would be better tested with a GUI, or by using integration tests as you did above.
To summarize, this section has explored:
- How to work with the Bank module and handle players making wagers on games, now that the application supports live games playing to completion (with the winner claiming both wagers) or expiring through inactivity (with the inactive player forfeiting their wager as if losing), and no possibility of withheld value being stranded in inactive games.
- How to add handling actions that ask the
bank
module to perform the token transfers required by the wager, and where to invoke them in the message handlers. - How to create a new wager-handling file with functions to collect a wager, refund a wager, and pay winnings, in which
must
prefixes indicate either a user-side error (leading to a failed transaction) or a failure of the application's escrow account (requiring the whole application be terminated). - How to run integration tests, which requires you to first build a proper bank keeper, create new helpers, refactor your existing keeper tests, account for the new events being emitted from the bank, and add extra checks of money handling.
- How to interact with the CLI to check account balances to test that wagers are being withheld and paid.