diff --git a/packages/stargate/src/accounts.spec.ts b/packages/stargate/src/accounts.spec.ts index 62d604436b..d2860eb7b4 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,76 @@ 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 95a765abfa..89890ccf13 100644 --- a/packages/stargate/src/accounts.ts +++ b/packages/stargate/src/accounts.ts @@ -40,49 +40,103 @@ function accountFromBaseAccount(input: BaseAccount): Account { */ export type AccountParser = (any: Any) => Account; +export type AccountParserRegistry = Map; + /** - * 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. + * 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 function accountFromAny(input: Any): Account { - const { typeUrl, value } = input; +export class AccountParserManager { + private readonly registry = new Map(); + + public constructor(initialRegistry: AccountParserRegistry = new Map()) { + this.registry = initialRegistry; + } - switch (typeUrl) { - // auth + /** Registers a new account parser for a specific typeUrl. */ + public register(typeUrl: Any["typeUrl"], parser: AccountParser): void { + this.registry.set(typeUrl, parser); + } - 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); + /** + * 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) { + throw new Error(`Unsupported type: '${input.typeUrl}'`); } + return parser(input); + } +} - // vesting +/** + * 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)); + }; - 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); - } + const parseModuleAccount: AccountParser = ({ value }: Any): Account => { + const baseAccount = ModuleAccount.decode(value).baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }; - default: - throw new Error(`Unsupported type: '${typeUrl}'`); - } + const parseBaseVestingAccount: AccountParser = ({ value }: Any): Account => { + const baseAccount = BaseVestingAccount.decode(value)?.baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }; + + const parseContinuousVestingAccount: AccountParser = ({ value }: Any): Account => { + const baseAccount = ContinuousVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }; + + const parseDelayedVestingAccount: AccountParser = ({ value }: Any): Account => { + const baseAccount = DelayedVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }; + + const parsePeriodicVestingAccount: AccountParser = ({ value }: Any): Account => { + const baseAccount = PeriodicVestingAccount.decode(value)?.baseVestingAccount?.baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + }; + + 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 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()); + return accountParser.parseAccount(input); } 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";