diff --git a/packages/proto-signing/src/decorator.spec.ts b/packages/proto-signing/src/decorator.spec.ts index 40f072bd..6c68aaba 100644 --- a/packages/proto-signing/src/decorator.spec.ts +++ b/packages/proto-signing/src/decorator.spec.ts @@ -3,8 +3,12 @@ import { assert } from "@cosmjs/utils"; import { Message } from "protobufjs"; import { cosmosField, cosmosMessage } from "./decorator"; +import { cosmos_sdk as cosmosSdk, google } from "./generated/codecimpl"; import { Registry } from "./registry"; +const { TxBody } = cosmosSdk.tx.v1; +const { Any } = google.protobuf; + describe("decorator demo", () => { it("works with a custom msg", () => { const nestedTypeUrl = "/demo.MsgNestedDemo"; @@ -44,8 +48,6 @@ describe("decorator demo", () => { const MsgNestedDemoT = myRegistry.lookupType(nestedTypeUrl)!; const MsgDemoT = myRegistry.lookupType(typeUrl)!; - const TxBody = myRegistry.lookupType("/cosmos.tx.TxBody")!; - const Any = myRegistry.lookupType("/google.protobuf.Any")!; const msgNestedDemoFields = { foo: "bar", diff --git a/packages/proto-signing/src/magic.spec.ts b/packages/proto-signing/src/magic.spec.ts new file mode 100644 index 00000000..17398a1f --- /dev/null +++ b/packages/proto-signing/src/magic.spec.ts @@ -0,0 +1,89 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { Message } from "protobufjs"; + +import { cosmosField, cosmosMessage } from "./decorator"; +import { Registry } from "./registry"; + +describe("registry magic demo", () => { + it("works with a custom msg", () => { + const nestedTypeUrl = "/demo.MsgNestedMagic"; + const typeUrl = "/demo.MsgMagic"; + const myRegistry = new Registry(); + + @cosmosMessage(myRegistry, nestedTypeUrl) + class MsgNestedMagic extends Message<{}> { + @cosmosField.string(1) + public readonly foo?: string; + } + + @cosmosMessage(myRegistry, typeUrl) + class MsgMagic extends Message<{}> { + @cosmosField.boolean(1) + public readonly booleanDemo?: boolean; + + @cosmosField.string(2) + public readonly stringDemo?: string; + + @cosmosField.bytes(3) + public readonly bytesDemo?: Uint8Array; + + @cosmosField.int64(4) + public readonly int64Demo?: number; + + @cosmosField.uint64(5) + public readonly uint64Demo?: number; + + @cosmosField.repeatedString(6) + public readonly listDemo?: readonly string[]; + + @cosmosField.message(7, MsgNestedMagic) + public readonly nestedDemo?: MsgNestedMagic; + } + + const msgNestedDemoFields = { + foo: "bar", + }; + const msgDemoFields = { + booleanDemo: true, + stringDemo: "example text", + bytesDemo: Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]), + int64Demo: -123, + uint64Demo: 123, + listDemo: ["this", "is", "a", "list"], + nestedDemo: msgNestedDemoFields, + }; + const txBodyFields = { + messages: [{ typeUrl: typeUrl, value: msgDemoFields }], + memo: "Some memo", + timeoutHeight: 9999, + extensionOptions: [], + }; + const txBodyBytes = myRegistry.encode({ + typeUrl: "/cosmos.tx.TxBody", + value: txBodyFields, + }); + + const txBodyDecoded = myRegistry.decode({ + typeUrl: "/cosmos.tx.TxBody", + value: txBodyBytes, + }); + expect(txBodyDecoded.memo).toEqual(txBodyFields.memo); + // int64Demo and uint64Demo decode to Long in Node + expect(Number(txBodyDecoded.timeoutHeight)).toEqual(txBodyFields.timeoutHeight); + expect(txBodyDecoded.extensionOptions).toEqual(txBodyFields.extensionOptions); + + const msgDemoDecoded = txBodyDecoded.messages[0] as MsgMagic; + expect(msgDemoDecoded).toBeInstanceOf(MsgMagic); + expect(msgDemoDecoded.booleanDemo).toEqual(msgDemoFields.booleanDemo); + expect(msgDemoDecoded.stringDemo).toEqual(msgDemoFields.stringDemo); + // bytesDemo decodes to a Buffer in Node + expect(Uint8Array.from(msgDemoDecoded.bytesDemo!)).toEqual(msgDemoFields.bytesDemo); + // int64Demo and uint64Demo decode to Long in Node + expect(Number(msgDemoDecoded.int64Demo)).toEqual(msgDemoFields.int64Demo); + expect(Number(msgDemoDecoded.uint64Demo)).toEqual(msgDemoFields.uint64Demo); + expect(msgDemoDecoded.listDemo).toEqual(msgDemoFields.listDemo); + + expect(msgDemoDecoded.nestedDemo).toBeInstanceOf(MsgNestedMagic); + expect(msgDemoDecoded.nestedDemo!.foo).toEqual(msgDemoFields.nestedDemo.foo); + }); +}); diff --git a/packages/proto-signing/src/registry.spec.ts b/packages/proto-signing/src/registry.spec.ts index c45ecb98..df31df02 100644 --- a/packages/proto-signing/src/registry.spec.ts +++ b/packages/proto-signing/src/registry.spec.ts @@ -2,20 +2,17 @@ import { assert } from "@cosmjs/utils"; import { MsgDemo as MsgDemoType } from "./demo"; -import { cosmos_sdk as cosmosSdk } from "./generated/codecimpl"; +import { cosmos_sdk as cosmosSdk, google } from "./generated/codecimpl"; import { Registry } from "./registry"; -type MsgDemo = { - readonly example: string; -}; +const { TxBody } = cosmosSdk.tx.v1; +const { Any } = google.protobuf; describe("registry demo", () => { it("works with a default msg", () => { const registry = new Registry(); const Coin = registry.lookupType("/cosmos.Coin")!; const MsgSend = registry.lookupType("/cosmos.bank.MsgSend")!; - const TxBody = registry.lookupType("/cosmos.tx.TxBody")!; - const Any = registry.lookupType("/google.protobuf.Any")!; const coin = Coin.create({ denom: "ucosm", @@ -57,8 +54,6 @@ describe("registry demo", () => { const typeUrl = "/demo.MsgDemo"; const registry = new Registry([[typeUrl, MsgDemoType]]); const MsgDemo = registry.lookupType(typeUrl)!; - const TxBody = registry.lookupType("/cosmos.tx.TxBody")!; - const Any = registry.lookupType("/google.protobuf.Any")!; const msgDemo = MsgDemo.create({ example: "Some example text", diff --git a/packages/proto-signing/src/registry.ts b/packages/proto-signing/src/registry.ts index b826bf70..a53cce2b 100644 --- a/packages/proto-signing/src/registry.ts +++ b/packages/proto-signing/src/registry.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/camelcase */ import protobuf from "protobufjs"; import { cosmos_sdk as cosmosSdk, google } from "./generated/codecimpl"; @@ -8,24 +9,109 @@ export interface GeneratedType { readonly decode: (reader: protobuf.Reader | Uint8Array, length?: number) => any; } +export interface EncodeObject { + readonly typeUrl: string; + readonly value: any; +} + +export interface DecodeObject { + readonly typeUrl: string; + readonly value: Uint8Array; +} + +export interface TxBodyValue { + readonly messages: readonly EncodeObject[]; + readonly memo?: string; + readonly timeoutHeight?: number; + readonly extensionOptions?: google.protobuf.IAny[]; + readonly nonCriticalExtensionOptions?: google.protobuf.IAny[]; +} + +const defaultTypeUrls = { + cosmosCoin: "/cosmos.Coin", + cosmosMsgSend: "/cosmos.bank.MsgSend", + cosmosTxBody: "/cosmos.tx.TxBody", + googleAny: "/google.protobuf.Any", +}; + export class Registry { private readonly types: Map; constructor(customTypes: Iterable<[string, GeneratedType]> = []) { + const { cosmosCoin, cosmosMsgSend } = defaultTypeUrls; this.types = new Map([ - ["/cosmos.Coin", cosmosSdk.v1.Coin], - ["/cosmos.bank.MsgSend", cosmosSdk.x.bank.v1.MsgSend], - ["/cosmos.tx.TxBody", cosmosSdk.tx.v1.TxBody], - ["/google.protobuf.Any", google.protobuf.Any], + [cosmosCoin, cosmosSdk.v1.Coin], + [cosmosMsgSend, cosmosSdk.x.bank.v1.MsgSend], ...customTypes, ]); } - public register(name: string, type: GeneratedType): void { - this.types.set(name, type); + public register(typeUrl: string, type: GeneratedType): void { + this.types.set(typeUrl, type); } - public lookupType(name: string): GeneratedType | undefined { - return this.types.get(name); + public lookupType(typeUrl: string): GeneratedType | undefined { + return this.types.get(typeUrl); + } + + private lookupTypeWithError(typeUrl: string): GeneratedType { + const type = this.lookupType(typeUrl); + if (!type) { + throw new Error(`Unregistered type url: ${typeUrl}`); + } + return type; + } + + public encode({ typeUrl, value }: EncodeObject): Uint8Array { + if (typeUrl === defaultTypeUrls.cosmosTxBody) { + return this.encodeTxBody(value); + } + const type = this.lookupTypeWithError(typeUrl); + const created = type.create(value); + return type.encode(created).finish(); + } + + public encodeTxBody(txBodyFields: TxBodyValue): Uint8Array { + const { TxBody } = cosmosSdk.tx.v1; + const { Any } = google.protobuf; + + const wrappedMessages = txBodyFields.messages.map((message) => { + const messageBytes = this.encode(message); + return Any.create({ + type_url: message.typeUrl, + value: messageBytes, + }); + }); + const txBody = TxBody.create({ + ...txBodyFields, + messages: wrappedMessages, + }); + return TxBody.encode(txBody).finish(); + } + + public decode({ typeUrl, value }: DecodeObject): any { + if (typeUrl === defaultTypeUrls.cosmosTxBody) { + return this.decodeTxBody(value); + } + const type = this.lookupTypeWithError(typeUrl); + return type.decode(value); + } + + public decodeTxBody(txBody: Uint8Array): cosmosSdk.tx.v1.TxBody { + const { TxBody } = cosmosSdk.tx.v1; + const decodedTxBody = TxBody.decode(txBody); + + return { + ...decodedTxBody, + messages: decodedTxBody.messages.map(({ type_url: typeUrl, value }: google.protobuf.IAny) => { + if (!typeUrl) { + throw new Error("Missing type_url in Any"); + } + if (!value) { + throw new Error("Missing value in Any"); + } + return this.decode({ typeUrl, value }); + }), + }; } } diff --git a/packages/proto-signing/types/registry.d.ts b/packages/proto-signing/types/registry.d.ts index 09ca8bba..777661b8 100644 --- a/packages/proto-signing/types/registry.d.ts +++ b/packages/proto-signing/types/registry.d.ts @@ -1,4 +1,5 @@ import protobuf from "protobufjs"; +import { cosmos_sdk as cosmosSdk, google } from "./generated/codecimpl"; export interface GeneratedType { readonly create: (properties?: { [k: string]: any }) => any; readonly encode: ( @@ -11,9 +12,29 @@ export interface GeneratedType { ) => protobuf.Writer; readonly decode: (reader: protobuf.Reader | Uint8Array, length?: number) => any; } +export interface EncodeObject { + readonly typeUrl: string; + readonly value: any; +} +export interface DecodeObject { + readonly typeUrl: string; + readonly value: Uint8Array; +} +export interface TxBodyValue { + readonly messages: readonly EncodeObject[]; + readonly memo?: string; + readonly timeoutHeight?: number; + readonly extensionOptions?: google.protobuf.IAny[]; + readonly nonCriticalExtensionOptions?: google.protobuf.IAny[]; +} export declare class Registry { private readonly types; constructor(customTypes?: Iterable<[string, GeneratedType]>); - register(name: string, type: GeneratedType): void; - lookupType(name: string): GeneratedType | undefined; + register(typeUrl: string, type: GeneratedType): void; + lookupType(typeUrl: string): GeneratedType | undefined; + private lookupTypeWithError; + encode({ typeUrl, value }: EncodeObject): Uint8Array; + encodeTxBody(txBodyFields: TxBodyValue): Uint8Array; + decode({ typeUrl, value }: DecodeObject): any; + decodeTxBody(txBody: Uint8Array): cosmosSdk.tx.v1.TxBody; }