Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make account parser implementation extendable #1530

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion packages/stargate/src/accounts.spec.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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,
});
});
});
});
128 changes: 91 additions & 37 deletions packages/stargate/src/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,49 +40,103 @@ function accountFromBaseAccount(input: BaseAccount): Account {
*/
export type AccountParser = (any: Any) => Account;

export type AccountParserRegistry = Map<Any["typeUrl"], AccountParser>;
Copy link
Author

@0xpatrickdev 0xpatrickdev Dec 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realizing we may also want to export accountFromBaseAccount (L26-L35), as this will likely be needed by those supplying a custom account parser. Alternatively, this could be baked into every parseAccount call as every AccountParser function likely needs this as the last step.

Will wait for feedback on this and the overall design before making any updates.


/**
* 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<Any["typeUrl"], AccountParser>();

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<Any["typeUrl"], AccountParser>([
["/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);
Comment on lines +134 to +136
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* const myAccountParserManager = new AccountParserManager(createAccountParserRegistry());
* myAccountParserManager.register('/custom.type.v1.CustomAccount', customAccountParser);
* const account = customParserManager.parseAccount(someInput);
* const { register, parseAccount } = new AccountParserManager(createAccountParserRegistry());
* register('/custom.type.v1.CustomAccount', customAccountParser);
* const options: StargateClientOptions = { accountParser: parseAccount };

This probably aligns closer to expected usage

* ```
*/
export function accountFromAny(input: Any): Account {
const accountParser = new AccountParserManager(createAccountParserRegistry());
return accountParser.parseAccount(input);
}
9 changes: 8 additions & 1 deletion packages/stargate/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down