Manually Registering Types
This document provides a reference for manually registering types in Telescope-generated code.
Overview
When working with Cosmos SDK applications, sometimes you need to extend Telescope's generated types with custom or third-party types. Manual type registration allows you to integrate these external types with Telescope's registry system.
Why Register Types Manually?
Manual type registration is necessary when:
- Using types from third-party libraries not generated by Telescope
- Working with custom types defined outside of Protocol Buffers
- Integrating with chains or modules that have specialized type requirements
- Creating hybrid applications with some manually defined types
Type Registry Basics
Telescope generates registries for each message type:
// Example of a generated registry
export const registry = [
["/cosmos.bank.v1beta1.MsgSend", MsgSend],
["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate],
// other message types...
];
Each entry pairs a type URL with its corresponding TypeScript implementation.
Manual Registration Methods
Method 1: Adding to Default Registry
import { registry } from "./codegen/cosmos/bank/v1beta1/tx";
import { GeneratedType } from "@cosmjs/proto-signing";
import { MyCustomType } from "./my-custom-types";
// Add your custom type to the registry
const customRegistry: Array<[string, GeneratedType]> = [
...registry,
["/my.custom.v1beta1.MyCustomType", MyCustomType]
];
// Use the extended registry when creating a client
const client = await SigningStargateClient.connectWithSigner(
rpcEndpoint,
signer,
{ registry: customRegistry }
);
Method 2: Using Registry Class
import { Registry } from "@cosmjs/proto-signing";
import { defaultRegistryTypes } from "@cosmjs/stargate";
import { MyCustomType } from "./my-custom-types";
// Create a new registry with default types
const registry = new Registry(defaultRegistryTypes);
// Register your custom types
registry.register("/my.custom.v1beta1.MyCustomType", MyCustomType);
// Use the registry
const client = await SigningStargateClient.connectWithSigner(
rpcEndpoint,
signer,
{ registry }
);
Method 3: Creating Registration Functions
import { Registry } from "@cosmjs/proto-signing";
import { MsgCustom } from "./my-module/types";
// Create a registration function
export function registerMyModuleTypes(registry: Registry): void {
registry.register("/my.module.v1beta1.MsgCustom", MsgCustom);
// Register additional types here
}
// Usage
const registry = new Registry(defaultRegistryTypes);
registerMyModuleTypes(registry);
Implementation Requirements
For a type to be registrable, it must implement the GeneratedType
interface:
interface GeneratedType {
readonly typeUrl: string;
readonly encode: (message: any) => Uint8Array;
readonly decode: (binary: Uint8Array) => any;
}
When creating custom types, ensure they have appropriate encoding and decoding functions.
Custom Type Example
import { Writer, Reader } from "protobufjs/minimal";
// Define your custom type interface
export interface CustomMessage {
field1: string;
field2: number;
}
// Implement the required methods
export const CustomMessage = {
typeUrl: "/my.custom.v1beta1.CustomMessage",
encode(message: CustomMessage, writer: Writer = Writer.create()): Writer {
if (message.field1 !== "") {
writer.uint32(10).string(message.field1);
}
if (message.field2 !== 0) {
writer.uint32(16).int32(message.field2);
}
return writer;
},
decode(input: Reader | Uint8Array, length?: number): CustomMessage {
const reader = input instanceof Reader ? input : new Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = { field1: "", field2: 0 } as CustomMessage;
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.field1 = reader.string();
break;
case 2:
message.field2 = reader.int32();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},
fromPartial(object: any): CustomMessage {
const message = { field1: "", field2: 0 } as CustomMessage;
if (object.field1 !== undefined && object.field1 !== null) {
message.field1 = object.field1;
}
if (object.field2 !== undefined && object.field2 !== null) {
message.field2 = object.field2;
}
return message;
}
};
Amino Type Registration
For chains that use Amino (legacy encoding), you'll also need to register Amino converters:
import { AminoTypes } from "@cosmjs/stargate";
import { AminoMsg } from "@cosmjs/amino";
// Define the Amino interface
interface AminoCustomMessage extends AminoMsg {
type: "my-module/CustomMessage";
value: {
field1: string;
field2: string; // Note: numbers are strings in Amino
};
}
// Create Amino converters
const aminoConverters = {
"/my.custom.v1beta1.CustomMessage": {
aminoType: "my-module/CustomMessage",
toAmino: ({ field1, field2 }: CustomMessage): AminoCustomMessage["value"] => ({
field1,
field2: field2.toString(),
}),
fromAmino: ({ field1, field2 }: AminoCustomMessage["value"]): CustomMessage => ({
field1,
field2: parseInt(field2, 10),
}),
},
};
// Register with AminoTypes
const aminoTypes = new AminoTypes(aminoConverters);
Global Decoder Registry
Telescope v1.0+ includes a global decoder registry system:
import { GlobalDecoderRegistry } from "./codegen/registry";
import { CustomMessage } from "./my-custom-types";
// Register a custom type
GlobalDecoderRegistry.register(CustomMessage.typeUrl, CustomMessage);
// Later, decode a message of any registered type
const decodedMessage = GlobalDecoderRegistry.decode(binaryData);
Using Custom Types with Telescope Clients
import { getSigningCosmosClient } from "./codegen/client";
import { CustomMessage } from "./my-custom-types";
import { Registry } from "@cosmjs/proto-signing";
// Create a registry with custom types
const registry = new Registry();
registry.register(CustomMessage.typeUrl, CustomMessage);
// Create the signing client
const client = await getSigningCosmosClient({
rpcEndpoint,
signer,
registry
});
// Now you can use custom message types
const customMsg = {
typeUrl: CustomMessage.typeUrl,
value: CustomMessage.fromPartial({
field1: "value",
field2: 42
})
};
// Use in transactions
const result = await client.signAndBroadcast(
signerAddress,
[customMsg],
fee
);
Best Practices
- Organize custom types in dedicated modules for better maintainability
- Follow the same patterns as Telescope-generated types for consistency
- Include proper TypeScript types for better IDE support
- Implement all required methods (encode, decode, fromPartial)
- Register both Proto and Amino types if working with wallets that use Amino
- Use meaningful type URLs that follow the pattern
/namespace.module.version.Type
- Cache registries where possible to avoid repeated registrations