From ffb1fa161db5810df56fbe977a527274163c0ae7 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 21 Dec 2023 19:53:32 -0500 Subject: [PATCH 1/4] feat: make accountParser implementation extendable - adds AccountParserManager class where consumers can register different accountParsers - adds createDefaultAccountParser() which instantiates AccountParserManager with common cosmos-sdk account types - preserves existing accountFromAny interface for backwards compatability --- packages/stargate/src/accounts.ts | 100 ++++++++++++++++++------------ 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/packages/stargate/src/accounts.ts b/packages/stargate/src/accounts.ts index 95a765abfa..0f1a53b328 100644 --- a/packages/stargate/src/accounts.ts +++ b/packages/stargate/src/accounts.ts @@ -40,49 +40,71 @@ function accountFromBaseAccount(input: BaseAccount): Account { */ export type AccountParser = (any: Any) => Account; -/** - * Basic implementation of AccountParser. This is supposed to support the most relevant - * common Cosmos SDK account types. If you need support for exotic account types, - * you'll need to write your own account decoder. - */ -export function accountFromAny(input: Any): Account { - const { typeUrl, value } = input; +export type AccountParserRegistry = Map; - switch (typeUrl) { - // auth +export class AccountParserManager { + private readonly registry = new Map(); - case "/cosmos.auth.v1beta1.BaseAccount": - return accountFromBaseAccount(BaseAccount.decode(value)); - case "/cosmos.auth.v1beta1.ModuleAccount": { - const baseAccount = ModuleAccount.decode(value).baseAccount; - assert(baseAccount); - return accountFromBaseAccount(baseAccount); - } + public constructor(initialRegistry: AccountParserRegistry = new Map()) { + this.registry = initialRegistry; + } - // vesting + public register(typeUrl: string, parser: AccountParser): void { + this.registry.set(typeUrl, parser); + } - case "/cosmos.vesting.v1beta1.BaseVestingAccount": { - const baseAccount = BaseVestingAccount.decode(value)?.baseAccount; - assert(baseAccount); - return accountFromBaseAccount(baseAccount); - } - case "/cosmos.vesting.v1beta1.ContinuousVestingAccount": { - const baseAccount = ContinuousVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; - assert(baseAccount); - return accountFromBaseAccount(baseAccount); - } - case "/cosmos.vesting.v1beta1.DelayedVestingAccount": { - const baseAccount = DelayedVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; - assert(baseAccount); - return accountFromBaseAccount(baseAccount); - } - case "/cosmos.vesting.v1beta1.PeriodicVestingAccount": { - const baseAccount = PeriodicVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; - assert(baseAccount); - return accountFromBaseAccount(baseAccount); + public parseAccount(input: Any): Account { + const parser = this.registry.get(input.typeUrl); + if (!parser) { + throw new Error(`Unsupported type: '${input.typeUrl}'`); } - - default: - throw new Error(`Unsupported type: '${typeUrl}'`); + return parser(input); } } + +export function createDefaultAccountParser(): AccountParserManager { + const registry = new AccountParserManager(); + // Register default parsers + // auth + registry.register("/cosmos.auth.v1beta1.BaseAccount", ({ value }: Any): Account => { + return accountFromBaseAccount(BaseAccount.decode(value)); + }); + registry.register("/cosmos.auth.v1beta1.ModuleAccount", ({ value }: Any): Account => { + const baseAccount = ModuleAccount.decode(value).baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }); + // vesting + registry.register("/cosmos.vesting.v1beta1.BaseVestingAccount", ({ value }: Any): Account => { + const baseAccount = BaseVestingAccount.decode(value)?.baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }); + registry.register("/cosmos.vesting.v1beta1.ContinuousVestingAccount", ({ value }: Any): Account => { + const baseAccount = ContinuousVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }); + registry.register("/cosmos.vesting.v1beta1.DelayedVestingAccount", ({ value }: Any): Account => { + const baseAccount = DelayedVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }); + registry.register("/cosmos.vesting.v1beta1.PeriodicVestingAccount", ({ value }: Any): Account => { + const baseAccount = PeriodicVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }); + + return registry; +} + +/** + * Basic implementation of AccountParser. This is supposed to support the most relevant + * common Cosmos SDK account types. If you need support for exotic account types, + * you'll need to use `AccountParserManager` or `createDefaultAccountParser` directly. + */ +export function accountFromAny(input: Any): Account { + const accountParser = createDefaultAccountParser(); + return accountParser.parseAccount(input); +} From 4cbb02887f941da46710873a88ac6311ff639faf Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 21 Dec 2023 21:49:59 -0500 Subject: [PATCH 2/4] feat: add tests for createAccountParserRegistry and AccountParserManager - refactor createDefaultAccountParser (instance) to createAccountParserRegistry (map of accountParsers) for easier testing --- packages/stargate/src/accounts.spec.ts | 72 +++++++++++++++++++++++++- packages/stargate/src/accounts.ts | 50 ++++++++++-------- 2 files changed, 100 insertions(+), 22 deletions(-) diff --git a/packages/stargate/src/accounts.spec.ts b/packages/stargate/src/accounts.spec.ts index 62d604436b..16697d17a0 100644 --- a/packages/stargate/src/accounts.spec.ts +++ b/packages/stargate/src/accounts.spec.ts @@ -1,7 +1,7 @@ import { fromBase64 } from "@cosmjs/encoding"; import { Any } from "cosmjs-types/google/protobuf/any"; -import { accountFromAny } from "./accounts"; +import { accountFromAny, AccountParser, AccountParserManager, createAccountParserRegistry } from "./accounts"; describe("accounts", () => { describe("accountFromAny", () => { @@ -26,4 +26,74 @@ describe("accounts", () => { }); }); }); + + describe("createAccountParserRegistry", () => { + it("returns a map of typeUrls and accountParsers", () => { + const defaultRegistry = createAccountParserRegistry(); + + const baseAccountParser = defaultRegistry.get("/cosmos.auth.v1beta1.BaseAccount"); + expect(baseAccountParser).toBeTruthy(); + expect(typeof baseAccountParser).toBe("function"); + + const baseVestingAccountParser = defaultRegistry.get("/cosmos.vesting.v1beta1.BaseVestingAccount"); + expect(baseVestingAccountParser).toBeTruthy(); + expect(typeof baseVestingAccountParser).toBe("function"); + }); + }); + + describe("AccountParserManager", () => { + it("registers new account parsers", () => { + const defaultRegistry = createAccountParserRegistry(); + const parsePeriodicVestingAccount = defaultRegistry.get("/cosmos.vesting.v1beta1.PeriodicVestingAccount"); + + const accountParser = new AccountParserManager(); + accountParser.register( + "/cosmos.vesting.v1beta1.PeriodicVestingAccount", + parsePeriodicVestingAccount as AccountParser, + ); + + // Queried from chain via `packages/cli/examples/get_akash_vesting_account.ts`. + const any = Any.fromPartial({ + typeUrl: "/cosmos.vesting.v1beta1.PeriodicVestingAccount", + value: fromBase64( + "CsMBCnoKLGFrYXNoMXF5MHZ1cjNmbDJ1Y3p0cHpjcmZlYTdtYzhqd3o4eGptdnE3cXZ5EkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohA/XsdhwSIKU73TltD9STcaS07FNw0szR4a+oDLr6vikaGDggGxIUCgR1YWt0EgwxNjY2NjY2NzAwMDAaEwoEdWFrdBILMzcxOTAzMzAwMDAiFAoEdWFrdBIMMTY2NjY2NjcwMDAwKOC9wZkGEODvt/sFGhoIgOeEDxITCgR1YWt0Egs4MzMzMzMzNTAwMBoaCIC/ugcSEwoEdWFrdBILNDE2NjY2Njc1MDAaGgiAqMoHEhMKBHVha3QSCzQxNjY2NjY3NTAw", + ), + }); + + const account = accountParser.parseAccount(any); + expect(account).toEqual({ + address: "akash1qy0vur3fl2ucztpzcrfea7mc8jwz8xjmvq7qvy", + pubkey: { + type: "tendermint/PubKeySecp256k1", + value: "A/XsdhwSIKU73TltD9STcaS07FNw0szR4a+oDLr6vika", + }, + accountNumber: 56, + sequence: 27, + }); + }); + + it("accepts a registry in its constructor", () => { + const defaultRegistry = createAccountParserRegistry(); + const accountParser = new AccountParserManager(defaultRegistry); + + // Queried from chain via `packages/cli/examples/get_akash_vesting_account.ts`. + const any = Any.fromPartial({ + typeUrl: "/cosmos.vesting.v1beta1.PeriodicVestingAccount", + value: fromBase64( + "CsMBCnoKLGFrYXNoMXF5MHZ1cjNmbDJ1Y3p0cHpjcmZlYTdtYzhqd3o4eGptdnE3cXZ5EkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohA/XsdhwSIKU73TltD9STcaS07FNw0szR4a+oDLr6vikaGDggGxIUCgR1YWt0EgwxNjY2NjY2NzAwMDAaEwoEdWFrdBILMzcxOTAzMzAwMDAiFAoEdWFrdBIMMTY2NjY2NjcwMDAwKOC9wZkGEODvt/sFGhoIgOeEDxITCgR1YWt0Egs4MzMzMzMzNTAwMBoaCIC/ugcSEwoEdWFrdBILNDE2NjY2Njc1MDAaGgiAqMoHEhMKBHVha3QSCzQxNjY2NjY3NTAw", + ), + }); + + const account = accountParser.parseAccount(any); + expect(account).toEqual({ + address: "akash1qy0vur3fl2ucztpzcrfea7mc8jwz8xjmvq7qvy", + pubkey: { + type: "tendermint/PubKeySecp256k1", + value: "A/XsdhwSIKU73TltD9STcaS07FNw0szR4a+oDLr6vika", + }, + accountNumber: 56, + sequence: 27, + }); + }); + }); }); diff --git a/packages/stargate/src/accounts.ts b/packages/stargate/src/accounts.ts index 0f1a53b328..9a01c8bb66 100644 --- a/packages/stargate/src/accounts.ts +++ b/packages/stargate/src/accounts.ts @@ -40,7 +40,7 @@ function accountFromBaseAccount(input: BaseAccount): Account { */ export type AccountParser = (any: Any) => Account; -export type AccountParserRegistry = Map; +export type AccountParserRegistry = Map; export class AccountParserManager { private readonly registry = new Map(); @@ -62,49 +62,57 @@ export class AccountParserManager { } } -export function createDefaultAccountParser(): AccountParserManager { - const registry = new AccountParserManager(); - // Register default parsers - // auth - registry.register("/cosmos.auth.v1beta1.BaseAccount", ({ value }: Any): Account => { +export function createAccountParserRegistry(): AccountParserRegistry { + const parseBaseAccount: AccountParser = ({ value }: Any): Account => { return accountFromBaseAccount(BaseAccount.decode(value)); - }); - registry.register("/cosmos.auth.v1beta1.ModuleAccount", ({ value }: Any): Account => { + }; + + const parseModuleAccount: AccountParser = ({ value }: Any): Account => { const baseAccount = ModuleAccount.decode(value).baseAccount; assert(baseAccount); return accountFromBaseAccount(baseAccount); - }); - // vesting - registry.register("/cosmos.vesting.v1beta1.BaseVestingAccount", ({ value }: Any): Account => { + }; + + const parseBaseVestingAccount: AccountParser = ({ value }: Any): Account => { const baseAccount = BaseVestingAccount.decode(value)?.baseAccount; assert(baseAccount); return accountFromBaseAccount(baseAccount); - }); - registry.register("/cosmos.vesting.v1beta1.ContinuousVestingAccount", ({ value }: Any): Account => { + }; + + const parseContinuousVestingAccount: AccountParser = ({ value }: Any): Account => { const baseAccount = ContinuousVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; assert(baseAccount); return accountFromBaseAccount(baseAccount); - }); - registry.register("/cosmos.vesting.v1beta1.DelayedVestingAccount", ({ value }: Any): Account => { + }; + + const parseDelayedVestingAccount: AccountParser = ({ value }: Any): Account => { const baseAccount = DelayedVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; assert(baseAccount); return accountFromBaseAccount(baseAccount); - }); - registry.register("/cosmos.vesting.v1beta1.PeriodicVestingAccount", ({ value }: Any): Account => { + }; + + const parsePeriodicVestingAccount: AccountParser = ({ value }: Any): Account => { const baseAccount = PeriodicVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; assert(baseAccount); return accountFromBaseAccount(baseAccount); - }); + }; - return registry; + return new Map([ + ["/cosmos.auth.v1beta1.BaseAccount", parseBaseAccount], + ["/cosmos.auth.v1beta1.ModuleAccount", parseModuleAccount], + ["/cosmos.vesting.v1beta1.BaseVestingAccount", parseBaseVestingAccount], + ["/cosmos.vesting.v1beta1.ContinuousVestingAccount", parseContinuousVestingAccount], + ["/cosmos.vesting.v1beta1.DelayedVestingAccount", parseDelayedVestingAccount], + ["/cosmos.vesting.v1beta1.PeriodicVestingAccount", parsePeriodicVestingAccount], + ]); } /** * Basic implementation of AccountParser. This is supposed to support the most relevant * common Cosmos SDK account types. If you need support for exotic account types, - * you'll need to use `AccountParserManager` or `createDefaultAccountParser` directly. + * you'll need to use `AccountParserManager` and `createAccountParserRegistry` directly. */ export function accountFromAny(input: Any): Account { - const accountParser = createDefaultAccountParser(); + const accountParser = new AccountParserManager(createAccountParserRegistry()); return accountParser.parseAccount(input); } From 9ba311a1c80aa7ca10e910d6a6d32de7f1c239cb Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 21 Dec 2023 21:51:01 -0500 Subject: [PATCH 3/4] feat: export createAccountParserRegistry function, AccountParserManager class, and AccountParserRegistry type --- packages/stargate/src/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index e424bed7c2..4e92062ed6 100644 --- a/packages/stargate/src/index.ts +++ b/packages/stargate/src/index.ts @@ -1,4 +1,11 @@ -export { Account, accountFromAny, AccountParser } from "./accounts"; +export { + Account, + accountFromAny, + AccountParser, + AccountParserManager, + AccountParserRegistry, + createAccountParserRegistry, +} from "./accounts"; export { AminoConverter, AminoConverters, AminoTypes } from "./aminotypes"; export { Attribute, Event, fromTendermintEvent } from "./events"; export { calculateFee, GasPrice } from "./fee"; From 53afd70d7751761cf21fcb835c5c392cb63fa8fb Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 21 Dec 2023 22:16:20 -0500 Subject: [PATCH 4/4] docs: add jsdoc descriptions for exported account functions --- packages/stargate/src/accounts.spec.ts | 4 +++- packages/stargate/src/accounts.ts | 30 +++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/stargate/src/accounts.spec.ts b/packages/stargate/src/accounts.spec.ts index 16697d17a0..d2860eb7b4 100644 --- a/packages/stargate/src/accounts.spec.ts +++ b/packages/stargate/src/accounts.spec.ts @@ -44,7 +44,9 @@ describe("accounts", () => { describe("AccountParserManager", () => { it("registers new account parsers", () => { const defaultRegistry = createAccountParserRegistry(); - const parsePeriodicVestingAccount = defaultRegistry.get("/cosmos.vesting.v1beta1.PeriodicVestingAccount"); + const parsePeriodicVestingAccount = defaultRegistry.get( + "/cosmos.vesting.v1beta1.PeriodicVestingAccount", + ); const accountParser = new AccountParserManager(); accountParser.register( diff --git a/packages/stargate/src/accounts.ts b/packages/stargate/src/accounts.ts index 9a01c8bb66..89890ccf13 100644 --- a/packages/stargate/src/accounts.ts +++ b/packages/stargate/src/accounts.ts @@ -42,17 +42,28 @@ export type AccountParser = (any: Any) => Account; export type AccountParserRegistry = Map; +/** + * AccountParserManager is a class responsible for managing a registry of account parsers. + * It allows registering new parsers and parsing account data using registered parsers. + * Once initialized, `AccountParserManager.parseAccount` can be provided for the `accountParser` + * option when initializing the StargateSigningCleint. + */ export class AccountParserManager { - private readonly registry = new Map(); + private readonly registry = new Map(); public constructor(initialRegistry: AccountParserRegistry = new Map()) { this.registry = initialRegistry; } - public register(typeUrl: string, parser: AccountParser): void { + /** Registers a new account parser for a specific typeUrl. */ + public register(typeUrl: Any["typeUrl"], parser: AccountParser): void { this.registry.set(typeUrl, parser); } + /** + * Parses an account from an `Any` encoded format using a registered parser based on the typeUrl. + * @throws Will throw an error if no parser is registered for the account's typeUrl. + */ public parseAccount(input: Any): Account { const parser = this.registry.get(input.typeUrl); if (!parser) { @@ -62,6 +73,12 @@ export class AccountParserManager { } } +/** + * Creates and returns a default registry of account parsers. + * Each parser is a function responsible for converting an `Any` encoded account + * from the chain into a common `Account` format. The registry maps `typeUrl` + * strings to corresponding `AccountParser` functions. + */ export function createAccountParserRegistry(): AccountParserRegistry { const parseBaseAccount: AccountParser = ({ value }: Any): Account => { return accountFromBaseAccount(BaseAccount.decode(value)); @@ -109,8 +126,15 @@ export function createAccountParserRegistry(): AccountParserRegistry { /** * Basic implementation of AccountParser. This is supposed to support the most relevant - * common Cosmos SDK account types. If you need support for exotic account types, + * common Cosmos SDK account types. If you need support for additional account types, * you'll need to use `AccountParserManager` and `createAccountParserRegistry` directly. + * + * @example + * ``` + * const myAccountParserManager = new AccountParserManager(createAccountParserRegistry()); + * myAccountParserManager.register('/custom.type.v1.CustomAccount', customAccountParser); + * const account = customParserManager.parseAccount(someInput); + * ``` */ export function accountFromAny(input: Any): Account { const accountParser = new AccountParserManager(createAccountParserRegistry());