v2-content
?
You are viewing an older version of the content, click here to switch to the current version
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 Stargate (opens new window) or cosmjs-types (opens new window) do is a good choice.

# Preparation

This exercise assumes that:

  1. Your Protobuf definition files are in ./proto/myChain.
  2. You want to compile them into TypeScript in ./client/src/types/generated.

Install protoc on your computer and its Typescript plugin in your project, possibly with the help of a Dockerfile:


You can confirm the version you received. The executable is located in ./node_modules/protoc/protoc/bin/protoc:

This returns something like:

Copy libprotoc 3.21.7

The compiler tools are ready. Time to use them.

Create the target folder if it does not exist yet:

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

# 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

# Compilation

You can now compile the Protobuf files. To avoid adding all the .proto files manually to the command, use xargs:


--proto_path is only ./proto so that your imports (such as import "cosmos/base...) can be found.

You should now see your files compiled into TypeScript. They have been correctly filed under their respective folders and contain both types and services definitions. It also created the compiled 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:

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).

# Proper saving

Commit the extra .proto files as well as the compiled ones to your repository so you do not need to recreate them.

Take inspiration from cosmjs-types codegen.sh (opens new window):

  1. Create a script file named ts-proto.sh with the previous command, or create a Makefile target.
  2. Add an npm run target (opens new window) with it, to keep track of how this was done and easily reproduce it in the future when you update a Protobuf file.

# 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 declare each string as a constant value, such as:

Copy export const msgSendTypeUrl = "/cosmos.bank.v1beta1.MsgSend";

Save those along with generated in ./client/src/types/modules.

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