# Create Custom Objects

Make sure you have everything you need before proceeding:

  • You understand the concepts of Protobuf.
  • You have completed the introductory CosmJS tutorial.
  • Go and npm are installed.
  • You have finished the 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.

With your checkers application ready for use, it is a good time to prepare client elements that eventually allow you to create a GUI and/or server-side scripts. Here, you will apply what you have learned about creating your own custom CosmJS interfaces.

Before you can get into working on your application directly, you need to make sure CosmJS understands your checkers module and knows how to interact with it. This generally means you need create the Protobuf objects and clients in TypeScript and create extensions that facilitate the use of them.

# Compile Protobuf

You will have to create a client folder that will contain all these new elements. If you want to keep the Go parts of your checkers project separate from the TypeScript parts, you can use another repository for the client. To keep a link between the two repositories, add the client parts as a submodule to your Go parts:

Copy $ git submodule add git@github.com:cosmos/academy-checkers-ui.git client

Replace the path with your own repository. In effect, this creates a new client folder. This client folder makes it possible for you to easily update another repository with content generated out of your Go code.

Create a folder named scripts in your project root. This is where you will launch the Protobuf compilation. In the scripts folder install modules for the Protobuf-to-TypeScript compiler:

Create the folder structure to receive the compiled files:

Copy $ mkdir -p client/src/types/generated

Check what Cosmos version you are using:

Copy $ grep cosmos-sdk go.mod

This may return:

Copy github.com/cosmos/cosmos-sdk v0.45.4

Download the required files from your .proto files:

Copy $ mkdir -p proto/cosmos/base/query/v1beta1 $ curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/v0.45.4/proto/cosmos/base/query/v1beta1/pagination.proto -o proto/cosmos/base/query/v1beta1/pagination.proto $ mkdir -p proto/google/api $ curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/v0.45.4/third_party/proto/google/api/annotations.proto -o proto/google/api/annotations.proto $ curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/v0.45.4/third_party/proto/google/api/http.proto -o proto/google/api/http.proto $ mkdir -p proto/gogoproto $ curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/v0.45.4/third_party/proto/gogoproto/gogo.proto -o proto/gogoproto/gogo.proto

Now compile:

You should now have your TypeScript files.

In order to easily repeat these steps in the future, you can add them to your existing Makefile with slight modifications:

Copy install-protoc-gen-ts: cd scripts && npm install mkdir -p scripts/protoc curl -L https://github.com/protocolbuffers/protobuf/releases/download/v21.5/protoc-21.5-linux-x86_64.zip -o scripts/protoc/protoc.zip cd scripts/protoc && unzip -o protoc.zip rm scripts/protoc/protoc.zip cosmos-version = v0.45.4 download-cosmos-proto: mkdir -p proto/cosmos/base/query/v1beta1 curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/${cosmos-version}/proto/cosmos/base/query/v1beta1/pagination.proto -o proto/cosmos/base/query/v1beta1/pagination.proto mkdir -p proto/google/api curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/${cosmos-version}/third_party/proto/google/api/annotations.proto -o proto/google/api/annotations.proto curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/${cosmos-version}/third_party/proto/google/api/http.proto -o proto/google/api/http.proto mkdir -p proto/gogoproto curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/${cosmos-version}/third_party/proto/gogoproto/gogo.proto -o proto/gogoproto/gogo.proto gen-protoc-ts: download-cosmos-proto install-protoc-gen-ts mkdir -p ./client/src/types/generated/ ls proto/checkers | xargs -I {} ./scripts/protoc/bin/protoc \ --plugin="./scripts/node_modules/.bin/protoc-gen-ts_proto" \ --ts_proto_out="./client/src/types/generated" \ --proto_path="./proto" \ --ts_proto_opt="esModuleInterop=true,forceLong=long,useOptionals=messages" \ checkers/{} Makefile View source

Then whenever you want to re-run them:

You have created the basic Protobuf objects (opens new window) that will assist you with communicating with the blockchain.

# Prepare integration

At this point, you have the generated files in your client folder. If you have made this client folder as a Git submodule, then you can work directly in it and do not need to go back to the checkers Cosmos SDK:

Copy $ cd client

Also, if you use Docker and did not go through the trouble of building the Docker image for the checkers Cosmos SDK, you can use the node:18.7 image.

Install the Protobuf.js package in your client project:

At a later stage, you will add checkers as an extension to Stargate, but you can define your checkers extension immediately. The canPlay query could make use of better types for player and position. Start by declaring them in client/src/checkers/player.ts:

Copy export type Player = "b" | "r" export type GamePiece = Player | "*" export interface Pos { x: number y: number } src types checkers player.ts View source

Your checkers extension will need to use the CosmJS Stargate package. Install it:

Now you can declare the checkers extension in src/modules/checkers/queries.ts:

Copy export interface AllStoredGameResponse { storedGames: StoredGame[] pagination?: PageResponse } export interface CheckersExtension { readonly checkers: { readonly getSystemInfo: () => Promise<SystemInfo> readonly getStoredGame: (index: string) => Promise<StoredGame | undefined> readonly getAllStoredGames: ( key: Uint8Array, offset: Long, limit: Long, countTotal: boolean, ) => Promise<AllStoredGameResponse> readonly canPlayMove: ( index: string, player: Player, from: Pos, to: Pos, ) => Promise<QueryCanPlayMoveResponse> } } src modules checkers queries.ts View source

Do not forget a setup function, as this is expected by Stargate:

Copy export function setupCheckersExtension(base: QueryClient): CheckersExtension { const rpc = createProtobufRpcClient(base) // Use this service to get easy typed access to query methods // This cannot be used for proof verification const queryService = new QueryClientImpl(rpc) return { checkers: { getSystemInfo: async (): Promise<SystemInfo> => { const { SystemInfo } = await queryService.SystemInfo({}) assert(SystemInfo) return SystemInfo }, getStoredGame: async (index: string): Promise<StoredGame | undefined> => { const response: QueryGetStoredGameResponse = await queryService.StoredGame({ index: index, }) return response.storedGame }, getAllStoredGames: async ( key: Uint8Array, offset: Long, limit: Long, countTotal: boolean, ): Promise<AllStoredGameResponse> => { const response: QueryAllStoredGameResponse = await queryService.StoredGameAll({ pagination: { key: key, offset: offset, limit: limit, countTotal: countTotal, reverse: false, }, }) return { storedGames: response.storedGame, pagination: response.pagination, } }, canPlayMove: async ( index: string, player: Player, from: Pos, to: Pos, ): Promise<QueryCanPlayMoveResponse> => { return queryService.CanPlayMove({ gameIndex: index, player: player, fromX: Long.fromNumber(from.x), fromY: Long.fromNumber(from.y), toX: Long.fromNumber(to.x), toY: Long.fromNumber(to.y), }) }, }, } } src modules checkers queries.ts View source

You may have to add these imports by hand:

Copy import { assert } from "@cosmjs/utils" import Long from "long" src modules checkers queries.ts View source

Now create your CheckersStargateClient in src/checkers_stargateclient.ts:

Copy export class CheckersStargateClient extends StargateClient { public readonly checkersQueryClient: CheckersExtension | undefined public static async connect( endpoint: string, options?: StargateClientOptions, ): Promise<CheckersStargateClient> { const tmClient = await Tendermint34Client.connect(endpoint) return new CheckersStargateClient(tmClient, options) } protected constructor(tmClient: Tendermint34Client | undefined, options: StargateClientOptions = {}) { super(tmClient, options) if (tmClient) { this.checkersQueryClient = QueryClient.withExtensions(tmClient, setupCheckersExtension) } } } src checkers_stargateclient.ts View source

# Integration tests

It is possible to already run some integration tests against a running checkers blockchain.

# Preparation

Install packages to run tests.

Describe how to connect to the running blockchain in a .env file in your project root:

Copy RPC_URL="http://localhost:26657" .env View source

Alternatively, use whichever address connects to the RPC port of the checkers blockchain. If your chain runs in a Docker container, you may need to pass your actual IP address.

This information will be picked up by the dotenv package. Now let TypeScript know about this in an environment.d.ts file:

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

Also add your tconfig.json as you see fit:

Copy { "exclude": ["./tests/", "./node_modules/", "./dist/"], "compilerOptions": { "esModuleInterop": true, "module": "ES2015", "moduleResolution": "node", "target": "ES6" } } tsconfig.json View source

Add the line that describes how the tests are run:

Copy { ... "scripts": { "test": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha --require ts-node/register 'test/**/*.ts'" }, ... } package.json View source

# First tests

Because the intention is to run these tests against a running chain they cannot expect too much, such as how many games have been created so far. Still, it is possible to at least test that the connection is made and queries pass through.

Create test/integration/system-info.ts:

Copy import { expect } from "chai" import { config } from "dotenv" import _ from "../../environment" import { CheckersStargateClient } from "../../src/checkers_stargateclient" import { CheckersExtension } from "../../src/modules/checkers/queries" config() describe("SystemInfo", function () { let client: CheckersStargateClient, checkers: CheckersExtension["checkers"] before("create client", async function () { client = await CheckersStargateClient.connect(process.env.RPC_URL) checkers = client.checkersQueryClient!.checkers }) it("can get system info", async function () { const systemInfo = await checkers.getSystemInfo() expect(systemInfo.nextId.toNumber()).to.be.greaterThanOrEqual(1) expect(parseInt(systemInfo.fifoHeadIndex, 10)).to.be.greaterThanOrEqual(-1) expect(parseInt(systemInfo.fifoTailIndex, 10)).to.be.greaterThanOrEqual(-1) }) }) test integration system-info.ts View source

And create one for stored games:

Copy import { expect } from "chai" import { config } from "dotenv" import Long from "long" import _ from "../../environment" import { CheckersStargateClient } from "../../src/checkers_stargateclient" import { CheckersExtension } from "../../src/modules/checkers/queries" config() describe("StoredGame", function () { let client: CheckersStargateClient, checkers: CheckersExtension["checkers"] before("create client", async function () { client = await CheckersStargateClient.connect(process.env.RPC_URL) checkers = client.checkersQueryClient!.checkers }) it("can get game list", async function () { const allGames = await checkers.getAllStoredGames( Uint8Array.of(), Long.fromInt(0), Long.fromInt(0), true, ) expect(allGames.storedGames).to.be.length.greaterThanOrEqual(0) }) it("cannot get non-existent game", async function () { try { await checkers.getStoredGame("no-id") expect.fail("It should have failed") } catch (error) { expect(error.toString()).to.equal( "Error: Query failed with (22): rpc error: code = NotFound desc = not found: key not found", ) } }) }) test integration stored-game.ts View source

Note the forced import of import _ from "../../environment", to actively inform on the string type (as opposed to string | undefined) and avoid any compilation error.

Launch your checkers chain, for instance from the checkers folder with:

Now if you run the tests:

This should return:

Copy StoredGame ✔ can get game list (39ms) ✔ cannot get non-existent game SystemInfo ✔ can get system info 3 passing (287ms)
synopsis

To summarize, this section has explored:

  • The need to prepare the elements that will eventually allow you to create a GUI and/or server-side scripts for your checkers application.
  • How to create the necessary Protobuf objects and clients in Typescript, the extensions that facilitate the use of these clients, so that CosmJS will understand and be able to interact with your checkers module.