From bb2bbcca2db93709629dd84a3bbd6e8b4f0337c3 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 30 Jun 2020 12:50:25 +0200 Subject: [PATCH] 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; }