proto-signing: Add encode/decode methods to registry

This commit is contained in:
willclarktech 2020-06-30 12:50:25 +02:00
parent c1770dbe0c
commit bb2bbcca2d
No known key found for this signature in database
GPG Key ID: 551A86E2E398ADF7
3 changed files with 208 additions and 10 deletions

View File

@ -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);
});
});

View File

@ -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<string, GeneratedType>;
constructor(customTypes: Iterable<[string, GeneratedType]> = []) {
const { cosmosCoin, cosmosMsgSend, cosmosTxBody, googleAny } = defaultTypeUrls;
this.types = new Map<string, GeneratedType>([
["/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 });
}),
};
}
}

View File

@ -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;
}