From bb2bbcca2db93709629dd84a3bbd6e8b4f0337c3 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 30 Jun 2020 12:50:25 +0200 Subject: [PATCH 1/4] proto-signing: Add encode/decode methods to registry --- packages/proto-signing/src/magic.spec.ts | 89 ++++++++++++++++++ packages/proto-signing/src/registry.ts | 104 +++++++++++++++++++-- packages/proto-signing/types/registry.d.ts | 25 ++++- 3 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 packages/proto-signing/src/magic.spec.ts diff --git a/packages/proto-signing/src/magic.spec.ts b/packages/proto-signing/src/magic.spec.ts new file mode 100644 index 00000000..f43b9192 --- /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.MsgNestedDemo"; + const typeUrl = "/demo.MsgDemo"; + const myRegistry = new Registry(); + + @cosmosMessage(myRegistry, nestedTypeUrl) + class MsgNestedDemo extends Message<{}> { + @cosmosField.string(1) + public readonly foo?: string; + } + + @cosmosMessage(myRegistry, typeUrl) + class MsgDemo 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, MsgNestedDemo) + public readonly nestedDemo?: MsgNestedDemo; + } + + 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 MsgDemo; + expect(msgDemoDecoded).toBeInstanceOf(MsgDemo); + 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(MsgNestedDemo); + expect(msgDemoDecoded.nestedDemo!.foo).toEqual(msgDemoFields.nestedDemo.foo); + }); +}); diff --git a/packages/proto-signing/src/registry.ts b/packages/proto-signing/src/registry.ts index b826bf70..ef210e70 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,111 @@ export interface GeneratedType { readonly decode: (reader: protobuf.Reader | Uint8Array, length?: number) => any; } +export type EncodeObject = { + readonly typeUrl: string; + readonly value: any; +}; + +export type DecodeObject = { + readonly typeUrl: string; + readonly value: Uint8Array; +}; + +export type TxBodyValue = { + readonly messages: readonly EncodeObject[]; + readonly memo?: string; + readonly timeoutHeight?: number; + readonly extensionOptions?: readonly any[]; + readonly nonCriticalExtensionOptions?: readonly any[]; +}; + +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, cosmosTxBody, googleAny } = 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], + [cosmosTxBody, cosmosSdk.tx.v1.TxBody], + [googleAny, google.protobuf.Any], ...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 = this.lookupTypeWithError(defaultTypeUrls.cosmosTxBody); + const Any = this.lookupTypeWithError(defaultTypeUrls.googleAny); + + 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 = this.lookupTypeWithError(defaultTypeUrls.cosmosTxBody); + 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..c7cb0ec8 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 } 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 declare type EncodeObject = { + readonly typeUrl: string; + readonly value: any; +}; +export declare type DecodeObject = { + readonly typeUrl: string; + readonly value: Uint8Array; +}; +export declare type TxBodyValue = { + readonly messages: readonly EncodeObject[]; + readonly memo?: string; + readonly timeoutHeight?: number; + readonly extensionOptions?: readonly any[]; + readonly nonCriticalExtensionOptions?: readonly any[]; +}; 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; } From 16e4b2906ea26da5dea9fe90efc8df8d612817c1 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 30 Jun 2020 15:08:30 +0200 Subject: [PATCH 2/4] proto-signing: Remove duplicate decorated types --- packages/proto-signing/src/magic.spec.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/proto-signing/src/magic.spec.ts b/packages/proto-signing/src/magic.spec.ts index f43b9192..17398a1f 100644 --- a/packages/proto-signing/src/magic.spec.ts +++ b/packages/proto-signing/src/magic.spec.ts @@ -6,18 +6,18 @@ import { Registry } from "./registry"; describe("registry magic demo", () => { it("works with a custom msg", () => { - const nestedTypeUrl = "/demo.MsgNestedDemo"; - const typeUrl = "/demo.MsgDemo"; + const nestedTypeUrl = "/demo.MsgNestedMagic"; + const typeUrl = "/demo.MsgMagic"; const myRegistry = new Registry(); @cosmosMessage(myRegistry, nestedTypeUrl) - class MsgNestedDemo extends Message<{}> { + class MsgNestedMagic extends Message<{}> { @cosmosField.string(1) public readonly foo?: string; } @cosmosMessage(myRegistry, typeUrl) - class MsgDemo extends Message<{}> { + class MsgMagic extends Message<{}> { @cosmosField.boolean(1) public readonly booleanDemo?: boolean; @@ -36,8 +36,8 @@ describe("registry magic demo", () => { @cosmosField.repeatedString(6) public readonly listDemo?: readonly string[]; - @cosmosField.message(7, MsgNestedDemo) - public readonly nestedDemo?: MsgNestedDemo; + @cosmosField.message(7, MsgNestedMagic) + public readonly nestedDemo?: MsgNestedMagic; } const msgNestedDemoFields = { @@ -72,8 +72,8 @@ describe("registry magic demo", () => { expect(Number(txBodyDecoded.timeoutHeight)).toEqual(txBodyFields.timeoutHeight); expect(txBodyDecoded.extensionOptions).toEqual(txBodyFields.extensionOptions); - const msgDemoDecoded = txBodyDecoded.messages[0] as MsgDemo; - expect(msgDemoDecoded).toBeInstanceOf(MsgDemo); + 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 @@ -83,7 +83,7 @@ describe("registry magic demo", () => { expect(Number(msgDemoDecoded.uint64Demo)).toEqual(msgDemoFields.uint64Demo); expect(msgDemoDecoded.listDemo).toEqual(msgDemoFields.listDemo); - expect(msgDemoDecoded.nestedDemo).toBeInstanceOf(MsgNestedDemo); + expect(msgDemoDecoded.nestedDemo).toBeInstanceOf(MsgNestedMagic); expect(msgDemoDecoded.nestedDemo!.foo).toEqual(msgDemoFields.nestedDemo.foo); }); }); From 15fdb34d911b7b7ed1cf0a80d7240be888372adb Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 30 Jun 2020 15:37:43 +0200 Subject: [PATCH 3/4] proto-signing: Remove TxBody and Any from registry --- packages/proto-signing/src/decorator.spec.ts | 6 ++++-- packages/proto-signing/src/registry.spec.ts | 11 +++-------- packages/proto-signing/src/registry.ts | 14 ++++++-------- packages/proto-signing/types/registry.d.ts | 6 +++--- 4 files changed, 16 insertions(+), 21 deletions(-) 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/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 ef210e70..e726b1f5 100644 --- a/packages/proto-signing/src/registry.ts +++ b/packages/proto-signing/src/registry.ts @@ -23,8 +23,8 @@ export type TxBodyValue = { readonly messages: readonly EncodeObject[]; readonly memo?: string; readonly timeoutHeight?: number; - readonly extensionOptions?: readonly any[]; - readonly nonCriticalExtensionOptions?: readonly any[]; + readonly extensionOptions?: google.protobuf.IAny[]; + readonly nonCriticalExtensionOptions?: google.protobuf.IAny[]; }; const defaultTypeUrls = { @@ -38,12 +38,10 @@ export class Registry { private readonly types: Map; constructor(customTypes: Iterable<[string, GeneratedType]> = []) { - const { cosmosCoin, cosmosMsgSend, cosmosTxBody, googleAny } = defaultTypeUrls; + const { cosmosCoin, cosmosMsgSend } = defaultTypeUrls; this.types = new Map([ [cosmosCoin, cosmosSdk.v1.Coin], [cosmosMsgSend, cosmosSdk.x.bank.v1.MsgSend], - [cosmosTxBody, cosmosSdk.tx.v1.TxBody], - [googleAny, google.protobuf.Any], ...customTypes, ]); } @@ -74,8 +72,8 @@ export class Registry { } public encodeTxBody(txBodyFields: TxBodyValue): Uint8Array { - const TxBody = this.lookupTypeWithError(defaultTypeUrls.cosmosTxBody); - const Any = this.lookupTypeWithError(defaultTypeUrls.googleAny); + const { TxBody } = cosmosSdk.tx.v1; + const { Any } = google.protobuf; const wrappedMessages = txBodyFields.messages.map((message) => { const messageBytes = this.encode(message); @@ -100,7 +98,7 @@ export class Registry { } public decodeTxBody(txBody: Uint8Array): cosmosSdk.tx.v1.TxBody { - const TxBody = this.lookupTypeWithError(defaultTypeUrls.cosmosTxBody); + const { TxBody } = cosmosSdk.tx.v1; const decodedTxBody = TxBody.decode(txBody); return { diff --git a/packages/proto-signing/types/registry.d.ts b/packages/proto-signing/types/registry.d.ts index c7cb0ec8..7be662d9 100644 --- a/packages/proto-signing/types/registry.d.ts +++ b/packages/proto-signing/types/registry.d.ts @@ -1,5 +1,5 @@ import protobuf from "protobufjs"; -import { cosmos_sdk as cosmosSdk } from "./generated/codecimpl"; +import { cosmos_sdk as cosmosSdk, google } from "./generated/codecimpl"; export interface GeneratedType { readonly create: (properties?: { [k: string]: any }) => any; readonly encode: ( @@ -24,8 +24,8 @@ export declare type TxBodyValue = { readonly messages: readonly EncodeObject[]; readonly memo?: string; readonly timeoutHeight?: number; - readonly extensionOptions?: readonly any[]; - readonly nonCriticalExtensionOptions?: readonly any[]; + readonly extensionOptions?: google.protobuf.IAny[]; + readonly nonCriticalExtensionOptions?: google.protobuf.IAny[]; }; export declare class Registry { private readonly types; From 8f933e171bddc51a8f0b37834b11693f48b62b92 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 30 Jun 2020 15:39:24 +0200 Subject: [PATCH 4/4] proto-signing: Make registry types interfaces --- packages/proto-signing/src/registry.ts | 12 ++++++------ packages/proto-signing/types/registry.d.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/proto-signing/src/registry.ts b/packages/proto-signing/src/registry.ts index e726b1f5..a53cce2b 100644 --- a/packages/proto-signing/src/registry.ts +++ b/packages/proto-signing/src/registry.ts @@ -9,23 +9,23 @@ export interface GeneratedType { readonly decode: (reader: protobuf.Reader | Uint8Array, length?: number) => any; } -export type EncodeObject = { +export interface EncodeObject { readonly typeUrl: string; readonly value: any; -}; +} -export type DecodeObject = { +export interface DecodeObject { readonly typeUrl: string; readonly value: Uint8Array; -}; +} -export type TxBodyValue = { +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", diff --git a/packages/proto-signing/types/registry.d.ts b/packages/proto-signing/types/registry.d.ts index 7be662d9..777661b8 100644 --- a/packages/proto-signing/types/registry.d.ts +++ b/packages/proto-signing/types/registry.d.ts @@ -12,21 +12,21 @@ export interface GeneratedType { ) => protobuf.Writer; readonly decode: (reader: protobuf.Reader | Uint8Array, length?: number) => any; } -export declare type EncodeObject = { +export interface EncodeObject { readonly typeUrl: string; readonly value: any; -}; -export declare type DecodeObject = { +} +export interface DecodeObject { readonly typeUrl: string; readonly value: Uint8Array; -}; -export declare type TxBodyValue = { +} +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]>);