v2-content
?
You are viewing an older version of the content, click here to switch to the current version
Filters

# Create Custom Messages

Make sure you have all you need before proceeding:

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:

Copy export const typeUrlMsgCreateGame = "/alice.checkers.checkers.MsgCreateGame" export const typeUrlMsgCreateGameResponse = "/alice.checkers.checkers.MsgCreateGameResponse" export const typeUrlMsgPlayMove = "/alice.checkers.checkers.MsgPlayMove" export const typeUrlMsgPlayMoveResponse = "/alice.checkers.checkers.MsgPlayMoveResponse" export const checkersTypes: ReadonlyArray<[string, GeneratedType]> = [ [typeUrlMsgCreateGame, MsgCreateGame], [typeUrlMsgCreateGameResponse, MsgCreateGameResponse], [typeUrlMsgPlayMove, MsgPlayMove], [typeUrlMsgPlayMoveResponse, MsgPlayMoveResponse], ] src types checkers messages.ts View source

Next proceed with the declarations. As with CosmJS, you can add an isMsgXX helper for each:

Copy export interface MsgCreateGameEncodeObject extends EncodeObject { readonly typeUrl: "/alice.checkers.checkers.MsgCreateGame" readonly value: Partial<MsgCreateGame> } export function isMsgCreateGameEncodeObject( encodeObject: EncodeObject, ): encodeObject is MsgCreateGameEncodeObject { return ( (encodeObject as MsgCreateGameEncodeObject).typeUrl === typeUrlMsgCreateGame ) } export interface MsgCreateGameResponseEncodeObject... {} export function isMsgCreateGameResponseEncodeObject(...)... export interface MsgPlayMoveEncodeObject... {} ... src types checkers messages.ts View source

This needs to be repeated for each of the messages that you require. To refresh your memory, that is:

# 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:

Copy import { defaultRegistryTypes } from "@cosmjs/stargate" export const checkersDefaultRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [ ...defaultRegistryTypes, ...checkersTypes, ] function createDefaultRegistry(): Registry { return new Registry(checkersDefaultRegistryTypes) } src checkers_signingstargateclient.ts View source

Similar to the read-only CheckersStargateClient, create a CheckersSigningStargateClient that inherits from SigningStargateClient:

Copy export class CheckersSigningStargateClient extends SigningStargateClient { public readonly checkersQueryClient: CheckersExtension | undefined public static async connectWithSigner( endpoint: string, signer: OfflineSigner, options: SigningStargateClientOptions = {}, ): Promise<CheckersSigningStargateClient> { const tmClient = await Tendermint34Client.connect(endpoint) return new CheckersSigningStargateClient(tmClient, signer, { registry: createDefaultRegistry(), ...options, }) } protected constructor( tmClient: Tendermint34Client | undefined, signer: OfflineSigner, options: SigningStargateClientOptions, ) { super(tmClient, signer, options) if (tmClient) { this.checkersQueryClient = QueryClient.withExtensions(tmClient, setupCheckersExtension) } } } src checkers_signingstargateclient.ts View source

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:

Copy public async createGame( creator: string, black: string, red: string, denom: string, wager: Long, fee: StdFee | "auto" | number, memo = "", ): Promise<DeliverTxResponse> { const createMsg: MsgCreateGameEncodeObject = { typeUrl: typeUrlMsgCreateGame, value: { black: black, red: red, creator: creator, denom: denom, wager: wager, }, } return this.signAndBroadcast(creator, [createMsg], fee, memo) } public async playMove( creator: string, gameIndex: string, from: Pos, to: Pos, fee: StdFee | "auto" | number, memo = "", ): Promise<DeliverTxResponse> { const playMoveMsg: MsgPlayMoveEncodeObject = { typeUrl: typeUrlMsgPlayMove, value: { creator: creator, gameIndex: gameIndex, fromX: Long.fromNumber(from.x), fromY: Long.fromNumber(from.y), toX: Long.fromNumber(to.x), toY: Long.fromNumber(to.y), }, } return this.signAndBroadcast(creator, [playMoveMsg], fee, memo) } src checkers_signingstargateclient.ts View source

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:

Copy declare global { namespace NodeJS { interface ProcessEnv { ... + 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:

Copy import { DirectSecp256k1HdWallet, OfflineDirectSigner } from "@cosmjs/proto-signing" export const getSignerFromMnemonic = async (mnemonic: string): Promise<OfflineDirectSigner> => { return DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "cosmos", }) } src util signer.ts View source

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", async function () { aliceSigner = await getSignerFromMnemonic(process.env.MNEMONIC_TEST_ALICE) bobSigner = await getSignerFromMnemonic(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:

With this confirmation, you can add another before that creates the signing clients:

Copy let aliceClient: CheckersSigningStargateClient, bobClient: CheckersSigningStargateClient, checkers: CheckersExtension["checkers"] before("create signing clients", async function () { aliceClient = await CheckersSigningStargateClient.connectWithSigner(RPC_URL, aliceSigner, { gasPrice: GasPrice.fromString("0stake"), }) bobClient = await CheckersSigningStargateClient.connectWithSigner(RPC_URL, bobSigner, { gasPrice: GasPrice.fromString("0stake"), }) checkers = aliceClient.checkersQueryClient!.checkers }) test integration stored-game-action.ts View source

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.

  1. If you use Ignite, it has created a faucet endpoint for you at port 4500. The page http://localhost:4500 explains the API.
  2. 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:

Copy declare global { namespace NodeJS { interface ProcessEnv { RPC_URL: string + FAUCET_URL: string ... } } } ... environment.d.ts View source

In a new, separate file, add:

  1. A http helper function:

    Copy export const httpRequest = async ( url: string | URL, options: RequestOptions, postData: string, ): Promise<string> => new Promise((resolve, reject) => { let all = "" const req = http.request(url, options, (response: IncomingMessage) => { response.setEncoding("utf8") response.on("error", reject) response.on("end", () => { if (400 <= response.statusCode!) reject(all) else resolve(all) }) response.on("data", (chunk) => (all = all + chunk)) }) req.write(postData) req.end() }) src util faucet.ts View source
  2. 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.
  3. A function that tries one faucet, and if that one fails tries the other:

    Copy export const askFaucet = async ( address: string, tokens: { [denom: string]: number }, ): Promise<string | string[]> => askFaucetComsJs(address, tokens).catch(() => askFaucetIgniteCli(address, tokens)) src util faucet.ts View source

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:

Copy const aliceCredit = { stake: 100, token: 1, }, bobCredit = { stake: 100, token: 1, } before("credit test accounts", async function () { this.timeout(40_000) if ( parseInt((await aliceClient.getBalance(alice, "stake")).amount, 10) < aliceCredit.stake || parseInt((await aliceClient.getBalance(alice, "token")).amount, 10) < aliceCredit.token ) await askFaucet(alice, aliceCredit) expect(parseInt((await aliceClient.getBalance(alice, "stake")).amount, 10)).to.be.greaterThanOrEqual( aliceCredit.stake, ) expect(parseInt((await aliceClient.getBalance(alice, "token")).amount, 10)).to.be.greaterThanOrEqual( aliceCredit.token, ) if ( parseInt((await bobClient.getBalance(bob, "stake")).amount, 10) < bobCredit.stake || parseInt((await bobClient.getBalance(bob, "token")).amount, 10) < bobCredit.token ) await askFaucet(bob, bobCredit) expect(parseInt((await bobClient.getBalance(bob, "stake")).amount, 10)).to.be.greaterThanOrEqual( bobCredit.stake, ) expect(parseInt((await bobClient.getBalance(bob, "token")).amount, 10)).to.be.greaterThanOrEqual( bobCredit.token, ) }) test integration stored-game-action.ts View source

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:

    Copy export type GameCreatedEvent = Event export const getCreateGameEvent = (log: Log): GameCreatedEvent | undefined => log.events?.find((event: Event) => event.type === "new-game-created") export const getCreatedGameId = (createdGameEvent: GameCreatedEvent): string => createdGameEvent.attributes.find((attribute: Attribute) => attribute.key == "game-index")!.value src types checkers events.ts View source
  • To get the captured position and the winner if they exist:

    Copy export type MovePlayedEvent = Event export const getMovePlayedEvent = (log: Log): MovePlayedEvent | undefined => log.events?.find((event: Event) => event.type === "move-played") export const getCapturedPos = (movePlayedEvent: MovePlayedEvent): Pos | undefined => { const x: number = parseInt( movePlayedEvent.attributes.find((attribute: Attribute) => attribute.key == "captured-x")!.value, 10, ) const y = parseInt( movePlayedEvent.attributes.find((attribute: Attribute) => attribute.key == "captured-y")!.value, 10, ) if (isNaN(x) || isNaN(y)) return undefined return { x, y } } export const getWinner = (movePlayedEvent: MovePlayedEvent): GamePiece | undefined => movePlayedEvent.attributes.find((attribute: Attribute) => attribute.key == "winner")!.value as | GamePiece | undefined src types checkers events.ts View source

    Here you also define GamePiece as:

    Copy export type GamePiece = Player | "*" src types checkers player.ts View source

# Create and play

Start by creating a game, extracting its index from the logs, and confirming that you can fetch it.

Copy let gameIndex: string it("can create game with wager", async function () { 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", async function () { 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:

  1. Will send many transactions in a single block.
  2. 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:

Copy export interface GameMove { player: Player from: Pos to: Pos } export const completeGame: GameMove[] = [ { player: "b", from: { x: 1, y: 2 }, to: { x: 2, y: 3 } }, { player: "r", from: { x: 0, y: 5 }, to: { x: 1, y: 4 } }, { player: "b", from: { x: 2, y: 3 }, to: { x: 0, y: 5 } }, { player: "r", from: { x: 4, y: 5 }, to: { x: 3, y: 4 } }, { player: "b", from: { x: 3, y: 2 }, to: { x: 2, y: 3 } }, { player: "r", from: { x: 3, y: 4 }, to: { x: 1, y: 2 } }, { player: "b", from: { x: 0, y: 1 }, to: { x: 2, y: 3 } }, { player: "r", from: { x: 2, y: 5 }, to: { x: 3, y: 4 } }, { player: "b", from: { x: 2, y: 3 }, to: { x: 4, y: 5 } }, { player: "r", from: { x: 5, y: 6 }, to: { x: 3, y: 4 } }, { player: "b", from: { x: 5, y: 2 }, to: { x: 4, y: 3 } }, { player: "r", from: { x: 3, y: 4 }, to: { x: 5, y: 2 } }, { player: "b", from: { x: 6, y: 1 }, to: { x: 4, y: 3 } }, { player: "r", from: { x: 6, y: 5 }, to: { x: 5, y: 4 } }, { player: "b", from: { x: 4, y: 3 }, to: { x: 6, y: 5 } }, { player: "r", from: { x: 7, y: 6 }, to: { x: 5, y: 4 } }, { player: "b", from: { x: 7, y: 2 }, to: { x: 6, y: 3 } }, { player: "r", from: { x: 5, y: 4 }, to: { x: 7, y: 2 } }, { player: "b", from: { x: 4, y: 1 }, to: { x: 3, y: 2 } }, { player: "r", from: { x: 3, y: 6 }, to: { x: 4, y: 5 } }, { player: "b", from: { x: 5, y: 0 }, to: { x: 4, y: 1 } }, { player: "r", from: { x: 2, y: 7 }, to: { x: 3, y: 6 } }, { player: "b", from: { x: 0, y: 5 }, to: { x: 2, y: 7 } }, { player: "r", from: { x: 4, y: 5 }, to: { x: 3, y: 4 } }, { player: "b", from: { x: 2, y: 7 }, to: { x: 4, y: 5 } }, // player captures again { player: "b", from: { x: 4, y: 5 }, to: { x: 2, y: 3 } }, { player: "r", from: { x: 6, y: 7 }, to: { x: 5, y: 6 } }, { player: "b", from: { x: 2, y: 3 }, to: { x: 3, y: 4 } }, { player: "r", from: { x: 0, y: 7 }, to: { x: 1, y: 6 } }, { player: "b", from: { x: 3, y: 2 }, to: { x: 4, y: 3 } }, { player: "r", from: { x: 7, y: 2 }, to: { x: 6, y: 1 } }, { player: "b", from: { x: 7, y: 0 }, to: { x: 5, y: 2 } }, { player: "r", from: { x: 1, y: 6 }, to: { x: 2, y: 5 } }, { player: "b", from: { x: 3, y: 4 }, to: { x: 1, y: 6 } }, { player: "r", from: { x: 4, y: 7 }, to: { x: 3, y: 6 } }, { player: "b", from: { x: 4, y: 3 }, to: { x: 3, y: 4 } }, { player: "r", from: { x: 5, y: 6 }, to: { x: 4, y: 5 } }, { player: "b", from: { x: 3, y: 4 }, to: { x: 5, y: 6 } }, { player: "r", from: { x: 3, y: 6 }, to: { x: 2, y: 5 } }, { player: "b", from: { x: 1, y: 6 }, to: { x: 3, y: 4 } }, ] src types checkers player.ts View source

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:

  1. 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.
  2. After each transaction, this sequence number increments, ready to be used for the next transaction of the account.
  3. The signing client's signAndbroadcast (opens new window) function fetches the sequence (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:

  1. It will start at the number as fetched from the blockchain.
  2. 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:

  1. Send the first 21 signed transactions with the fast this.forceGetTmClient().broadcastTxSync.
  2. 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:

Copy interface ShortAccountInfo { accountNumber: number sequence: number } const getShortAccountInfo = async (who: string): Promise<ShortAccountInfo> => { const accountInfo: Account = (await aliceClient.getAccount(who))! return { accountNumber: accountInfo.accountNumber, sequence: accountInfo.sequence, } } test integration stored-game-action.ts View source

Add helpers to pick the right Alice or Bob values:

Copy const whoseClient = (who: Player) => (who == "b" ? aliceClient : bobClient) const whoseAddress = (who: Player) => (who == "b" ? alice : bob) test integration stored-game-action.ts View source

Add a function to your client to give you access to CometBFT's broadcastTxSync:

Copy public async tmBroadcastTxSync(tx: Uint8Array): Promise<BroadcastTxSyncResponse> { return this.forceGetTmClient().broadcastTxSync({ tx }) } src checkers_stargateclient.ts View source

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", async function () { this.timeout(20_000) const client: CheckersStargateClient = await CheckersStargateClient.connect(RPC_URL) const chainId: string = await client.getChainId() const accountInfo = { b: await getShortAccountInfo(alice), r: await getShortAccountInfo(bob), } // TODO }) test integration stored-game-action.ts View source

Now get all 22 signed transactions, from index 2 to index 23:

Copy const txList: TxRaw[] = [] let txIndex: number = 2 while (txIndex < 24) { const gameMove: GameMove = completeGame[txIndex] txList.push( await whoseClient(gameMove.player).sign( whoseAddress(gameMove.player), [ { typeUrl: typeUrlMsgPlayMove, value: { creator: whoseAddress(gameMove.player), gameIndex: gameIndex, fromX: gameMove.from.x, fromY: gameMove.from.y, toX: gameMove.to.x, toY: gameMove.to.y, }, }, ], { amount: [{ denom: "stake", amount: "0" }], gas: "500000", }, `playing move ${txIndex}`, { accountNumber: accountInfo[gameMove.player].accountNumber, sequence: accountInfo[gameMove.player].sequence++, chainId: chainId, }, ), ) txIndex++ } test integration stored-game-action.ts View source

Note how:

  1. The moves 0 and 1 (opens new window) already took place in the previous it test.
  2. The gas fee can no longer be "auto" and has to be set, here at a price of 0. You may have to adjust this depending on your running chain.
  3. The sequence number of the signer is increased after it has been used: .sequence++.
  4. There is an await on .sign, but this is very fast because the signing happens without contacting the blockchain.
  5. 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:

Copy const hashes: BroadcastTxSyncResponse[] = [] txIndex = 0 while (txIndex < txList.length - 1) { const txRaw: TxRaw = txList[txIndex] hashes.push(await client.tmBroadcastTxSync(TxRaw.encode(txRaw).finish())) txIndex++ } test integration stored-game-action.ts View source

You now normally broadcast the last one:

Copy const lastDelivery: DeliverTxResponse = await client.broadcastTx( TxRaw.encode(txList[txList.length - 1]).finish(), ) test integration stored-game-action.ts View source

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:

Copy const game: StoredGame = (await checkers.getStoredGame(gameIndex))! expect(game.board).to.equal("*b*b***b|**b*b***|***b***r|********|***r****|********|***r****|r*B*r*r*") test integration stored-game-action.ts View source

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:

Copy it("can send a double capture move", async function () { this.timeout(10_000) const firstCaptureMove: GameMove = completeGame[24] const secondCaptureMove: GameMove = completeGame[25] // TODO }) test integration stored-game-action.ts View source

In it, make the call with the correctly crafted messages.

Copy const response: DeliverTxResponse = await aliceClient.signAndBroadcast( alice, [ { typeUrl: typeUrlMsgPlayMove, value: { creator: alice, gameIndex: gameIndex, fromX: firstCaptureMove.from.x, fromY: firstCaptureMove.from.y, toX: firstCaptureMove.to.x, toY: firstCaptureMove.to.y, }, }, { typeUrl: typeUrlMsgPlayMove, value: { creator: alice, gameIndex: gameIndex, fromX: secondCaptureMove.from.x, fromY: secondCaptureMove.from.y, toX: secondCaptureMove.to.x, toY: secondCaptureMove.to.y, }, }, ], "auto", ) test integration stored-game-action.ts View source

Next, collect the events and confirm they match your expectations:

Copy const logs: Log[] = JSON.parse(response.rawLog!) expect(logs).to.be.length(2) expect(getCapturedPos(getMovePlayedEvent(logs[0])!)).to.deep.equal({ x: 3, y: 6, }) expect(getCapturedPos(getMovePlayedEvent(logs[1])!)).to.deep.equal({ x: 3, y: 4, }) test integration stored-game-action.ts View source

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:

    1. Get the Dockerfile:

      Copy $ curl -O https://raw.githubusercontent.com/cosmos/b9-checkers-academy-draft/main/Dockerfile-standalone
    2. 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:

Copy $ docker stop cosmos-faucet checkers $ docker network rm checkers-net
synopsis

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.