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.
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:
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:
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.
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?
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:
Copy
RPC_URL="http://localhost:26657"
+ MNEMONIC_TEST_ALICE="theory arrow blue much illness carpet arena thought clay office path museum idea text foot bacon until tragic inform stairs pitch danger spatial slight"
+ ADDRESS_TEST_ALICE="cosmos1fx6qlxwteeqxgxwsw83wkf4s9fcnnwk8z86sql"
+ MNEMONIC_TEST_BOB="apple spoil melody venture speed like dawn cherry insane produce carry robust duck language next electric episode clinic acid sheriff video knee spoil multiply"
+ ADDRESS_TEST_BOB="cosmos1mql9aaux3453tdghk6rzkmk43stxvnvha4nv22"
.env View source
Copy
RPC_URL="http://checkers:26657"
+ MNEMONIC_TEST_ALICE="theory arrow blue much illness carpet arena thought clay office path museum idea text foot bacon until tragic inform stairs pitch danger spatial slight"
+ ADDRESS_TEST_ALICE="cosmos1fx6qlxwteeqxgxwsw83wkf4s9fcnnwk8z86sql"
+ MNEMONIC_TEST_BOB="apple spoil melody venture speed like dawn cherry insane produce carry robust duck language next electric episode clinic acid sheriff video knee spoil multiply"
+ ADDRESS_TEST_BOB="cosmos1mql9aaux3453tdghk6rzkmk43stxvnvha4nv22"
.env View source
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:
Copy
declare global {namespace NodeJS {interfaceProcessEnv{...+MNEMONIC_TEST_ALICE:string+ADDRESS_TEST_ALICE:string+MNEMONIC_TEST_BOB:string+ADDRESS_TEST_BOB:string}}}... environment.d.ts View source
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:
Copy
const{RPC_URL,ADDRESS_TEST_ALICE: alice,ADDRESS_TEST_BOB: bob }= process.env
let aliceSigner: OfflineDirectSigner, bobSigner: OfflineDirectSigner
before("create signers",asyncfunction(){
aliceSigner =awaitgetSignerFromMnemonic(process.env.MNEMONIC_TEST_ALICE)
bobSigner =awaitgetSignerFromMnemonic(process.env.MNEMONIC_TEST_BOB)expect((await aliceSigner.getAccounts())[0].address).to.equal(alice)expect((await bobSigner.getAccounts())[0].address).to.equal(bob)}) test integration stored-game-action.ts View source
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:
Copy
$ npm test
Copy
$ docker run --rm -it \
-v $(pwd):/client \
-w /client \
node:18.7-slim \
npm test
With this confirmation, you can add another before that creates the signing clients:
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 page http://localhost:4500 explains the API.
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.
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:
Start by creating a game, extracting its index from the logs, and confirming that you can fetch it.
Copy
let gameIndex:stringit("can create game with wager",asyncfunction(){this.timeout(10_000)const response: DeliverTxResponse =await aliceClient.createGame(
alice,
alice,
bob,"token",
Long.fromNumber(1),"auto",)const logs: Log[]=JSON.parse(response.rawLog!)expect(logs).to.be.length(1)
gameId =getCreatedGameId(getCreateGameEvent(logs[0])!)const game: StoredGame =(await checkers.getStoredGame(gameId))!expect(game).to.include({
index: gameId,
black: alice,
red: bob,
denom:"token",})expect(game.wager.toNumber()).to.equal(1)}) test integration stored-game-action.ts View source
Next, add a test that confirms that the wager tokens are consumed on first play:
Copy
it("can play first moves and pay wager",asyncfunction(){this.timeout(20_000)const aliceBalBefore =parseInt((await aliceClient.getBalance(alice,"token")).amount,10)await aliceClient.playMove(alice, gameIndex,{ x:1, y:2},{ x:2, y:3},"auto")expect(parseInt((await aliceClient.getBalance(alice,"token")).amount,10)).to.be.equal(
aliceBalBefore -1,)const bobBalBefore =parseInt((await aliceClient.getBalance(bob,"token")).amount,10)await bobClient.playMove(bob, gameIndex,{ x:0, y:5},{ x:1, y:4},"auto")expect(parseInt((await aliceClient.getBalance(bob,"token")).amount,10)).to.be.equal(
bobBalBefore -1,)}) test integration stored-game-action.ts View source
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.
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.
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.
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 fastthis.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:
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:
Copy
it("can continue the game up to before the double capture",asyncfunction(){this.timeout(20_000)const client: CheckersStargateClient =await CheckersStargateClient.connect(RPC_URL)const chainId:string=await client.getChainId()const accountInfo ={
b:awaitgetShortAccountInfo(alice),
r:awaitgetShortAccountInfo(bob),}// TODO}) test integration stored-game-action.ts View source
Now get all 22 signed transactions, from index 2 to index 23:
If you are interested, you can log the blocks in which the transactions were included:
Copy
console.log(
txList.length,"transactions included in blocks from",(await client.getTx(toHex(hashes[0].hash)))!.height,"to",
lastDelivery.height,) test integration stored-game-action.ts View source
Lastly, make sure that the game has the expected board:
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.
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).
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:
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:
Note how RPC_URL and FAUCET_URL override the default values found in .env, typically localhost.
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:
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.