You have the finished checkers blockchain exercise. If not, you can follow that tutorial here or just clone and checkout the relevant branch(opens new window) that contains the final version.
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 three 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 can reuse the setup you prepared in the previous section. There is an added difficulty: because you send transactions, your tests need access to keys. 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
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 test 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:
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. If you use Ignite, it has created a faucet endpoint for you at port 4500. The page http://localhost:4500 explains how to make the calls. Use that.
You will find out with practice how many tokens your accounts need for the tests. Start with any value. Create another before that will credit Alice and Bob from the faucet and confirm that they are rich enough to continue:
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(5_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)
gameIndex =getCreatedGameId(getCreateGameEvent(logs[0])!)const game: StoredGame =(await checkers.getStoredGame(gameIndex))!expect(game).to.include({
index: gameIndex,
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(10_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 transactions 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, or something like array.forEach(() => { await }), which fire 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 Tendermint 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 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 Tendermint includes the transactions in the order in which they were submitted. If Tendermint 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:
Create your it test with the necessary initializations:
Copy
it("can continue the game up to before the double capture",asyncfunction(){this.timeout(10_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",
lastDeliver.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 a message for creation and a message to make the first move on it. That's because the index of the game is not known before the transaction has been included in a block, and with that the index computed.
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, rejecting a game, or playing a move.