Filters

# Create Custom CosmJS Interfaces

In this section, you will:

  • Create custom CosmJS interfaces to connect to custom Cosmos SDK modules.
  • Define custom interfaces with Protobuf.
  • Define custom types and messages.
  • Integrate with Ignite - previously known as Starport.

CosmJS comes out of the box with interfaces that connect with the standard Cosmos SDK modules such as bank and gov and understand the way their messages are serialized. Since your own blockchain's modules are unique, they need custom CosmJS interfaces. That process consists of several steps:

  1. Creating the Protobuf objects and clients in TypeScript.
  2. Creating extensions that facilitate the use of the above clients.
  3. Any further level of abstraction that you deem useful for integration.

This section assumes that you have a working Cosmos blockchain with its own modules. It is based on CosmJS version v0.28.3 (opens new window).

# Compiling the Protobuf objects and clients

You can choose which library you use to compile your Protobuf objects into TypeScript or JavaScript. Reproducing what cosmjs-types (opens new window) or Stargate (opens new window) do is a good choice.

# Preparation

This exercise assumes that:

  1. The library you're creating is in 'myLib'.
  2. Your Protobuf definition files are in ./proto/.
  3. You want to transpile them into TypeScript in ./src/codegen/.

Install telescope, in your package or in Docker:


You can confirm the version you received by running:

This returns something like:

Copy 1.0.5

The compiler tools are ready. Time to use them.

# Getting third party files

You need to get the imports that appear in your .proto files. Usually you can find the following in query.proto (opens new window):

Copy import "cosmos/base/query/v1beta1/pagination.proto"; import "gogoproto/gogo.proto"; import "google/api/annotations.proto";

You need local copies of the right file versions in the right locations. Pay particular attention to Cosmos SDK's version of your project. You can check by running:

Copy $ grep cosmos-sdk go.mod

This returns something like:

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

Use this version as a tag on Github. One way to retrieve the pagination file (opens new window) is:

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

You can do the same for the others, found in the third_party folder (opens new window) under the same version:

Copy $ 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

Or you can add the latest whole third_party folders by running npx telescope install under myLib folder:

# Compilation

You can now compile the Protobuf files using Telescope.

You should now see some .ts files generated in ./src/codegen. These are the real source files used in your application.

By default, the command takes ./proto as input folder and ./src/codegen as output folder. You can config in and out folders and the way Telescope generates code by using options like these examples:

When running the command, Telescope takes a proto folder as input, and generates files in a 'gen/src' folder.

Each time telescope transpile has run, a config file, .telescope.json, will be created in the folder where it is running:

Copy //.telescope.json { "protoDirs": [ "./proto" ], "outPath": "gen/src", "options": { // telescope options "addTypeUrlToDecoders": true, "addTypeUrlToObjects": true, "typingsFormat": { "num64": "bigint", "customTypes": { "useCosmosSDKDec": true } }, ... } }
  • protoDirs: root directory that contains folders with Protobuf files. For example, if protoDirs is set with proto and proto-common, when importing from cosmos/base/query/v1beta1/pagination.proto, Telescope will check whether there is proto/cosmos/base/query/v1beta1/pagination.proto or proto-common/cosmos/base/query/v1beta1/pagination.proto, and then generate a dependency based on the file.
  • outPath: contains TypeScript files structured by the folder structure of Protobuf files.

You can modify the config file according to Telescope options (opens new window), and then use the file by the option --config:

  • protoDirs are taken both from --protoDirs arguments and the protoDirs field in the configuration file. It means proto files inside proto-common and proto will be taken in this case.
  • outPath is taken only from the configuration file, gen/src in this case. It means that even if the --outPath option is provided to the command, the value will be ignored.

After it has run successfully, there will be a message:

Copy ✨ files transpiled in '/gen/src' ✨ transpilation successful!

outPath is created automatically during the transpilation. This means that you will get no "missing dir" error, for instance if your outPath is incorrect.


You should now see your files transpiled into TypeScript. They have been correctly filed under their respective folders and contain both types and services definitions. It also created the transpiled versions of your third party imports.

# A note about the result

Your tx.proto file may have contained the following:

Copy service Msg { rpc Send(MsgSend) returns (MsgSendResponse); //... } proto cosmos ... v1beta1 tx.proto View source

If so, you find its service declaration in the compiled tx.ts file (with Telescope option rpcClients.inline: true) or in tx.rpc.msg.ts file (with Telescope option rpcClients.inline: false):

Copy export interface Msg { Send(request: MsgSend): Promise<MsgSendResponse>; //... } src cosmos ... v1beta1 tx.ts View source

It also appears in the default implementation:

Copy export class MsgClientImpl implements Msg { private readonly rpc: Rpc; constructor(rpc: Rpc) { this.rpc = rpc; this.Send = this.Send.bind(this); //... } Send(request: MsgSend): Promise<MsgSendResponse> { const data = MsgSend.encode(request).finish(); const promise = this.rpc.request("cosmos.bank.v1beta1.Msg", "Send", data); return promise.then((data) => MsgSendResponse.decode(new _m0.Reader(data))); } //... } src cosmos ... v1beta1 tx.ts View source

The important points to remember from this are:

  1. rpc: RPC is an instance of a Protobuf RPC client that is given to you by CosmJS. Although the interface appears to be declared locally (opens new window), this is the same interface found throughout CosmJS (opens new window). It is given to you on construction (opens new window). At this point you do not need an implementation for it.
  2. You can see encode and decode in action. Notice the .finish() that flushes the Protobuf writer buffer.
  3. The rpc.request makes calls that are correctly understood by the Protobuf compiled server on the other side.

You can find the same structure in query.ts (opens new window). (or query.rpc.Query.ts with telescope option rpcClients.inline: false)

# Proper saving

Commit the extra .proto files and the compiled ones to your repository so you do not need to recreate them. Add an npm run target to keep track of how this was done and easily reproduce it in the future when you update a Protobuf file:

Copy "scripts": { "codegen": "telescope transpile --config .telescope.json", }

# Add convenience with types

CosmJS provides an interface to which all the created types conform, TsProtoGeneratedType (opens new window), which is itself a sub-type of GeneratedType (opens new window). In the same file, note the definition:

Copy export interface EncodeObject { readonly typeUrl: string; readonly value: any; } packages proto-signing src registry.ts View source

The typeUrl is the identifier by which Protobuf identifies the type of the data to serialize or deserialize. It is composed of the type's package and its name. For instance (and see also here (opens new window)):

Copy package cosmos.bank.v1beta1; //... message MsgSend { //... }

In this case, the MsgSend's type URL is "/cosmos.bank.v1beta1.MsgSend" (opens new window).

Each of your types is associated like this. You can also find constant strings like those in generated files if prototypes.addTypeUrlToObjects is set to true, take tx.ts (opens new window) as an example:

Copy export const MsgSend = { // there's no this field in the given link, but there will be if prototypes.addTypeUrlToObjects is set to true. typeUrl: "/cosmos.bank.v1beta1.MsgSend", ... }

# For messages

Messages, sub-types of Msg, are assembled into transactions that are then sent to CometBFT. CosmJS types already include types for transactions (opens new window). These are assembled, signed, and sent by the SigningStargateClient (opens new window) of CosmJS.

The Msg kind also needs to be added to a registry. To facilitate that, you should prepare them in a nested array:

Copy export const bankTypes: ReadonlyArray<[string, GeneratedType]> = [ ["/cosmos.bank.v1beta1.MsgMultiSend", MsgMultiSend], ["/cosmos.bank.v1beta1.MsgSend", MsgSend], ]; packages stargate ... bank messages.ts View source

Add child types to EncodeObject to direct TypeScript:

Copy export interface MsgSendEncodeObject extends EncodeObject { readonly typeUrl: "/cosmos.bank.v1beta1.MsgSend"; readonly value: Partial<MsgSend>; } packages stargate ... bank messages.ts View source

In the previous code, you cannot reuse your msgSendTypeUrl because it is a value not a type. You can add a type helper, which is useful in an if else situation:

Copy export function isMsgSendEncodeObject(encodeObject: EncodeObject): encodeObject is MsgSendEncodeObject { return (encodeObject as MsgSendEncodeObject).typeUrl === "/cosmos.bank.v1beta1.MsgSend"; } packages stargate ... bank messages.ts View source

# For queries

Queries have very different types of calls. It makes sense to organize them in one place, called an extension. For example:

Copy export interface BankExtension { readonly bank: { readonly balance: (address: string, denom: string) => Promise<Coin>; readonly allBalances: (address: string) => Promise<Coin[]>; //... }; } packages stargate ... bank queries.ts View source

Note that there is a key bank: inside it. This becomes important later on when you add it to Stargate.

  1. Create an extension interface for your module using function names and parameters that satisfy your needs.
  2. It is recommended to make sure that the key is unique and does not overlap with any other modules of your application.
  3. Create a factory for its implementation copying the model here (opens new window). Remember that the QueryClientImpl (opens new window) implementation must come from your own compiled Protobuf query service.

# Integration with Stargate

StargateClient and SigningStargateClient are typically the ultimate abstractions that facilitate the querying and sending of transactions. You are now ready to add your own elements to them. The easiest way is to inherit from them and expose the extra functions you require.

If your extra functions map one-for-one with those of your own extension, then you can publicly expose the extension itself to minimize duplication in StargateClient (opens new window) and SigningStargateClient (opens new window).

For example, if you have your interface MyExtension with a myKey key and you are creating MyStargateClient:

Copy export class MyStargateClient extends StargateClient { public readonly myQueryClient: MyExtension | undefined public static async connect( endpoint: string, options: StargateClientOptions = {}, ): Promise<MyStargateClient> { const tmClient = await Tendermint34Client.connect(endpoint) return new MyStargateClient(tmClient, options) } protected constructor(tmClient: Tendermint34Client | undefined, options: StargateClientOptions) { super(tmClient, options) if (tmClient) { this.myQueryClient = QueryClient.withExtensions(tmClient, setupMyExtension) } } }

You can extend StargateClientOptions (opens new window) if your own client can receive further options.

You also need to inform MySigningStargateClient about the extra encodable types it should be able to handle. The list is defined in a registry that you can pass as options (opens new window).

Take inspiration from the SigningStargateClient source code (opens new window) itself. Collect your new types into an array:

Copy import { defaultRegistryTypes } from "@cosmjs/stargate" export const myDefaultRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [ ...defaultRegistryTypes, ...myTypes, // As you defined bankTypes earlier ]

Taking inspiration from the same place (opens new window), add the registry creator:

Copy function createDefaultRegistry(): Registry { return new Registry(myDefaultRegistryTypes) }

Now you are ready to combine this into your own MySigningStargateClient. It still takes an optional registry, but if that is missing it adds your newly defined default one:

Copy export class MySigningStargateClient extends SigningStargateClient { public readonly myQueryClient: MyExtension | undefined public static async connectWithSigner( endpoint: string, signer: OfflineSigner, options: SigningStargateClientOptions = {} ): Promise<MySigningStargateClient> { const tmClient = await Tendermint34Client.connect(endpoint) return new MySigningStargateClient(tmClient, signer, { registry: createDefaultRegistry(), ...options, }) } protected constructor(tmClient: Tendermint34Client | undefined, signer: OfflineSigner, options: SigningStargateClientOptions) { super(tmClient, signer, options) if (tmClient) { this.myQueryClient = QueryClient.withExtensions(tmClient, setupMyExtension) } } }

You can optionally add dedicated functions that use your own types, modeled on:

Copy public async sendTokens( senderAddress: string, recipientAddress: string, amount: readonly Coin[], fee: StdFee | "auto" | number, memo = "", ): Promise<DeliverTxResponse> { const sendMsg: MsgSendEncodeObject = { typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: { fromAddress: senderAddress, toAddress: recipientAddress, amount: [...amount], }, }; return this.signAndBroadcast(senderAddress, [sendMsg], fee, memo); } packages stargate src signingstargateclient.ts View source

Think of your functions as examples of proper use, that other developers can reuse when assembling more complex transactions.

You are ready to import and use this in a server script or a GUI.

If you would like to get started on building your own CosmJS elements on your own checkers game, you can go straight to the exercise in CosmJS for Your Chain to start from scratch.

More specifically, you can jump to:

synopsis

To summarize, this section has explored:

  • How CosmJS's out-of-the-box interfaces understand how messages of standard Cosmos SDK modules are serialized, meaning that your unique modules will require custom CosmJS interfaces of their own.
  • How to create the necessary Protobuf objects and clients in TypeScript, the extensions that facilitate the use of these clients, and any further level of abstraction that you deem useful for integration.
  • How to integrate CosmJS with Ignite's client and signing client, which are typically the ultimate abstractions that facilitate the querying and sending of transactions.