# Create Custom Messages
Make sure you have all you need before proceeding:
- You understand the concepts of CosmJS.
- You have generated the necessary TypeScript types in the previous tutorial. If not, just clone and checkout the relevant branch (opens new window).
In the previous section, you created the objects that allow you to query your checkers blockchain. Now, you will create the elements that allow you to send transactions to it.
# Encodable messages
Previously you defined in Protobuf two messages and their respective responses. You had Protobuf compile them (opens new window). Now you will create a few instances of EncodeObject
, similar to how this is done in CosmJS's bank module (opens new window). First, collect their names and Protobuf packages. Each Protobuf type identifier is assigned its encodable type:
Next proceed with the declarations. As with CosmJS, you can add an isMsgXX
helper for each:
This needs to be repeated for each of the messages that you require. To refresh your memory, that is:
MsgCreateGame
(opens new window)MsgCreateGameResponse
(opens new window)MsgPlayMove
(opens new window)MsgPlayMoveResponse
(opens new window)
# A signing Stargate for checkers
This process again takes inspiration from SigningStargateClient
(opens new window). Prepare by registering your new types in addition to the others, so that your client knows them all:
Similar to the read-only CheckersStargateClient
, create a CheckersSigningStargateClient
that inherits from SigningStargateClient
:
Note the use of createDefaultRegistry
as the default registry, if nothing was passed via the options.
# The action methods
Finally you ought to add the methods that allow you to interact with the blockchain. They also help advertise how to craft messages for your client. Taking inspiration from sendTokens
(opens new window), create one function for each of your messages:
You should not consider these two functions as the only ones that users of CheckersSigningStargateClient
should ever use. Rather, see them as a demonstration regarding to how to build messages properly.
# Integration tests
You can reuse the setup you prepared in the previous section. However, there is an added difficulty: because you send transactions, your tests need access to keys and tokens. How do you provide them in a testing context?
# Key preparation
You would not treat mainnet keys in this way, but here you save testing keys on disk. Update .env
with the test mnemonics of your choice:
If you use different mnemonics and do not yet know the corresponding addresses, you can get them from the before
action (below) when it fails. Also adjust environment.d.ts
to inform the TypeScript compiler:
In a new separate file, define how you build a signer from the mnemonic:
Create a new stored-game-action.ts
integration test file, modeled on stored-game.ts
, that starts by preparing the signers and confirms the match between mnemonics and first addresses:
These early verifications do not need any running chain, so go ahead and make sure they pass. Add a temporary empty it
test and run:
With this confirmation, you can add another before
that creates the signing clients:
If the running chain allows it, and to make your life easier, you can set the gas price to 0
. If not, set it as low as possible.
# Token preparation
Just saving keys on disk does not magically make these keys hold tokens on your test blockchain. You need to fund them at their addresses, using the funds of other addresses of your running chain. A faucet, if one is set up, is convenient for this step.
- If you use Ignite, it has created a faucet endpoint for you at port
4500
. The pagehttp://localhost:4500
explains the API. - If you use the CosmJS faucet (opens new window), you can make it serve on port
4500
too.
For the purpose of this exercise, you will prepare for both faucets.
Add the faucet(s) address in .env
:
Also add the faucet address to environment.d.ts
:
In a new, separate file, add:
A
http
helper function:Two helper functions to easily call the faucets of CosmJS (opens new window) and Ignite:
- The CosmJS faucet API does not let you specify the desired amount.
- The Ignite faucet API does let you specify the desired amount.
A function that tries one faucet, and if that one fails tries the other:
You will find out with practice how many tokens your accounts need for their tests. Start with any value. Ignite's default configuration is to start a chain with two tokens: stake
and token
. This is a good default, as you can use both denoms.
Create another before
that will credit Alice and Bob from the faucet so that they are rich enough to continue:
Your accounts are now ready to proceed with the tests proper.
There are an extra 40 seconds given for this potentially slower process: this.timeout(40_000)
.
You may want to adjust this time-out value. Here it is set at 10 seconds multiplied by the maximum number of transactions in the function. Here there are at most 4 transactions when calling the CosmJS faucet. Query calls are typically very fast, and therefore need not enter in the time-out calculation.
# Adding tests
Since these integration tests make calls to a running chain, they need to run one after the other. And if one it
fails, all the it
tests that come after will fail too. This is not ideal but is how these examples will work.
With a view to reusing them, add convenience methods that encapsulate the extraction of information from the events:
To get the id of the game created:
To get the captured position and the winner if they exist:
Here you also define
GamePiece
as:
# Create and play
Start by creating a game, extracting its index from the logs, and confirming that you can fetch it.
Next, add a test that confirms that the wager tokens are consumed on first play:
These first tests demonstrate the use of the createGame
and playMove
functions you created. These functions send a single message per transaction, and wait for the transaction to be included in a block before moving on.
In the next paragraphs you:
- Will send many transactions in a single block.
- Will send a transaction with more than one message in it.
The transaction with more than one message that you will send is where Alice, the black player, captures two red pieces in two successive moves (opens new window). The checkers rules allow this.
The many transactions per block will be those that make the game reach that point.
Prepare the game moves of a complete game:
Note how you already played the first two.
# Multiple transactions in a block
You are going to send 22 transactions (opens new window) in as quick a succession as possible. If you waited for each to be included in a block, it would take you in the order of 22*5 == 110
seconds. That's very long for a test. It is better to find a way to include more transactions per block.
You will face several difficulties when you want to send multiple transactions in a single block. The first difficulty is as follows:
- Each transaction signed by an account must mention the correct
sequence
of that account at the time of inclusion in the block. This is to make sure the transactions of a given account are added in the right order and to prevent transaction replay. - After each transaction, this
sequence
number increments, ready to be used for the next transaction of the account. - The signing client's
signAndbroadcast
(opens new window) function fetches thesequence
(opens new window) number from the blockchain.
In other words, the signing client can only know about the transactions that have been included in a block. It has no idea whether there are already pending transactions with a higher sequence
that would result in the account's sequence
being higher when your poorly crafted transaction is checked, therefore causing it to be rejected.
Fortunately, the sign
(opens new window) function can take any sequence number (opens new window). You will therefore force the sequence number to the one you know to be right eventually:
- It will start at the number as fetched from the blockchain.
- Whenever you sign a new transaction you will increment this sequence number and keep track of it in your own variable.
Because JavaScript has low assurances when it comes to threading, you need to make sure that each sign
command happens after the previous one, or your sequence
incrementing may get messed up. For that, you should not use Promise.all
on something like array.forEach(() => { await })
, which fires all promises roughly at the same time. Instead you will use a while() { await }
pattern.
There is a second difficulty when you want to send that many signed transactions. The client's broadcastTx
function waits for it (opens new window) to be included in a block, which would defeat the purpose of signing separately. Fortunately, if you look into its content, you can see that it calls this.forceGetTmClient().broadcastTxSync
(opens new window). This CometBFT client function returns only the hash (opens new window), that is before any inclusion in a block.
On the other hand, you want the last transaction to be included in the block so that when you query for the stored game you get the expected values. Therefore you will:
- Send the first 21 signed transactions with the fast
this.forceGetTmClient().broadcastTxSync
. - Send the last signed transaction with a slow client
broadcastTx
.
Here again, you need to make sure that you submit all transactions in sequential manner, otherwise a player may in effect try to play before their turn. At this point, you trust that CometBFT includes the transactions in the order in which they were submitted. If CometBFT does any shuffling between Alice and Bob, you may end up with a "play before their turn" error.
With luck, all transactions may end up in a single block, which would make the test 22 times faster than if you had waited for each transaction to get into its own block.
You would use the same techniques if you wanted to stress test your blockchain. This is why these paragraphs are more than just entertainment.
Add a way to track the sequences of Alice and Bob:
Add helpers to pick the right Alice or Bob values:
Add a function to your client to give you access to CometBFT's broadcastTxSync
:
Note that this function is on the read-only Stargate client. The signing Stargate client also holds a signer, but because here the signing is taking place outside Stargate, it is reasonable to add tmBroadcastTxSync
in the Stargate that has the easiest constructor.
Create your it
test with the necessary initializations:
Now get all 22 signed transactions, from index 2 to index 23:
Note how:
- The moves
0
and1
(opens new window) already took place in the previousit
test. - The gas fee can no longer be
"auto"
and has to be set, here at a price of0
. You may have to adjust this depending on your running chain. - The sequence number of the signer is increased after it has been used:
.sequence++
. - There is an
await
on.sign
, but this is very fast because the signing happens without contacting the blockchain. - The message has to be built at a low level, bypassing the convenient
playMove
method.
With all the transactions signed, you can fire broadcast the first 21 of them:
You now normally broadcast the last one:
If you are interested, you can log the blocks in which the transactions were included:
Lastly, make sure that the game has the expected board:
You have now brought the game up to the point just before the black player can capture two pieces in two moves.
# Combine messages
Alice, the black player can capture two pieces in one turn (opens new window). The checkers rules allow that.
You are now ready to send that one transaction with the two messages with the use of signAndBroadcast
. Add an it
test with the right initializations:
In it, make the call with the correctly crafted messages.
Next, collect the events and confirm they match your expectations:
Note how it checks the logs for the captured attributes. In effect, a captured piece has x
and y
as the average of the respective from
and to
positions' fields.
Sending a single transaction with two moves is cheaper and faster, from the point of view of the player, than sending two separate ones for the same effect.
It is not possible for Alice, who is the creator and black player, to send in a single transaction both a message for creation and a message to make the first move on it. This is because the index of the game is not known before the transaction has been included in a block, and with that the index computed.
Of course, she could try to do this. However, if her move failed because of a wrong game id, then the whole transaction would revert, and that would include the game creation being reverted.
Worse, a malicious attacker could front-run Alice's transaction with another transaction, creating a game where Alice is also the black player and whose id ends up being the one Alice signed in her first move. In the end she would make the first move on a game she did not really intend to play. This game could even have a wager that is all of Alice's token holdings.
# Further tests
You can add further tests, for instance to see what happens with token balances when you continue playing the game up to its completion (opens new window).
# Prepare your checkers chain
If you launch the tests just like you did in the previous section, you may be missing a faucet.
Adjust what you did.
If you came here after going through the rest of the hands-on exercise, you know how to launch a running chain with Ignite, which has a faucet to start with.
If you arrived here and are only focused on learning CosmJS, it is possible to abstract away niceties of both the running chain and a faucet in a minimal package. For this, you need Docker and to create an image:
Get the
Dockerfile
:Build the Docker images:
If you have another preferred method, make sure to keep track of the required RPC_URL
and FAUCET_URL
accordingly.
If you are curious about how this Dockerfile-standalone
was created, head to the run in production section.
# Launch the tests
Launch your checkers chain and the faucet. You can choose your preferred method, as long as they can be accessed at the RPC_URL
and FAUCET_URL
you defined earlier. For the purposes of this exercise, you have the choice between three methods:
Now you can run the tests:
The only combination of running chain / running tests that will not work with the above is if you run Ignite on your local computer and the tests in a container. For this edge case, you should put your host IP address in RPC_URL
and FAUCET_URL
, for instance --env RPC_URL="http://YOUR-HOST-IP:26657"
.
If you started the chain in Docker, when you are done you can stop the containers with:
To summarize, this section has explored:
- How to create the elements necessary for you to begin sending transactions to your checkers blockchain, including encodable messages, a signing client, and action methods that permit interaction with the blockchain.
- How to test your signing client, including key preparation (with either a mnemonic or a private key) and client preparation, followed by functions such as creating a game, or playing a move.
- How to send multiple transactions in one block, simulating a stress test.
- How to send multiple messages in a single transaction.