diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index 71cd826e..99f39b4e 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -84,7 +84,7 @@ export { uint64ToString, } from "./lcdapi"; export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs"; -export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; +export { decodeAminoPubkey, decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; diff --git a/packages/launchpad/src/pubkey.ts b/packages/launchpad/src/pubkey.ts index 2095c05a..82753525 100644 --- a/packages/launchpad/src/pubkey.ts +++ b/packages/launchpad/src/pubkey.ts @@ -21,9 +21,7 @@ const pubkeyAminoPrefixEd25519 = fromHex("1624de6420"); const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005"); const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length; -export function decodeBech32Pubkey(bechEncoded: string): PubKey { - const { data } = Bech32.decode(bechEncoded); - +export function decodeAminoPubkey(data: Uint8Array): PubKey { const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength); const rest = data.slice(pubkeyAminoPrefixLength); if (equal(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { @@ -55,6 +53,11 @@ export function decodeBech32Pubkey(bechEncoded: string): PubKey { } } +export function decodeBech32Pubkey(bechEncoded: string): PubKey { + const { data } = Bech32.decode(bechEncoded); + return decodeAminoPubkey(data); +} + export function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string { let aminoPrefix: Uint8Array; switch (pubkey.type) { diff --git a/packages/launchpad/types/index.d.ts b/packages/launchpad/types/index.d.ts index a4dce608..8a873502 100644 --- a/packages/launchpad/types/index.d.ts +++ b/packages/launchpad/types/index.d.ts @@ -82,7 +82,7 @@ export { uint64ToString, } from "./lcdapi"; export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs"; -export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; +export { decodeAminoPubkey, decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; diff --git a/packages/launchpad/types/pubkey.d.ts b/packages/launchpad/types/pubkey.d.ts index b6ebdebc..2319d77f 100644 --- a/packages/launchpad/types/pubkey.d.ts +++ b/packages/launchpad/types/pubkey.d.ts @@ -1,4 +1,5 @@ import { PubKey } from "./types"; export declare function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey; +export declare function decodeAminoPubkey(data: Uint8Array): PubKey; export declare function decodeBech32Pubkey(bechEncoded: string): PubKey; export declare function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string; diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 484a1eb8..b6e8996d 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -1,5 +1,7 @@ +import { assert } from "@cosmjs/utils"; + import { StargateClient } from "./stargateclient"; -import { nonExistentAddress, pendingWithoutSimapp, simapp, unused } from "./testutils.spec"; +import { nonExistentAddress, pendingWithoutSimapp, simapp, unused, validator } from "./testutils.spec"; describe("StargateClient", () => { describe("connect", () => { @@ -11,14 +13,71 @@ describe("StargateClient", () => { }); }); + describe("getAccount", () => { + it("works for unused account", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + + const account = await client.getAccount(unused.address); + assert(account); + expect(account).toEqual({ + address: unused.address, + pubkey: null, + accountNumber: unused.accountNumber, + sequence: unused.sequence, + }); + + client.disconnect(); + }); + + it("works for account with pubkey and non-zero sequence", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + + const account = await client.getAccount(validator.address); + assert(account); + expect(account).toEqual({ + address: validator.address, + pubkey: validator.pubkey, + accountNumber: validator.accountNumber, + sequence: validator.sequence, + }); + + client.disconnect(); + }); + + it("returns null for non-existent address", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + + const account = await client.getAccount(nonExistentAddress); + expect(account).toBeNull(); + + client.disconnect(); + }); + }); + describe("getSequence", () => { it("works for unused account", async () => { pendingWithoutSimapp(); const client = await StargateClient.connect(simapp.tendermintUrl); - const { accountNumber, sequence } = await client.getSequence(unused.address); - expect(accountNumber).toEqual(unused.accountNumber); - expect(sequence).toEqual(unused.sequence); + const account = await client.getSequence(unused.address); + assert(account); + expect(account).toEqual({ + accountNumber: unused.accountNumber, + sequence: unused.sequence, + }); + + client.disconnect(); + }); + + it("returns null for non-existent address", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + + const account = await client.getSequence(nonExistentAddress); + expect(account).toBeNull(); client.disconnect(); }); diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 70257be2..7ee31b9f 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Bech32, toAscii, toHex } from "@cosmjs/encoding"; -import { Coin } from "@cosmjs/launchpad"; +import { Coin, decodeAminoPubkey, PubKey } from "@cosmjs/launchpad"; import { Uint64 } from "@cosmjs/math"; import { decodeAny } from "@cosmjs/proto-signing"; import { Client as TendermintClient } from "@cosmjs/tendermint-rpc"; @@ -9,7 +9,15 @@ import Long from "long"; import { cosmos } from "./generated/codecimpl"; -export interface GetSequenceResult { +export interface Account { + /** Bech32 account address */ + readonly address: string; + readonly pubkey: PubKey | null; + readonly accountNumber: number; + readonly sequence: number; +} + +export interface SequenceResponse { readonly accountNumber: number; readonly sequence: number; } @@ -18,6 +26,18 @@ function uint64FromProto(input: number | Long): Uint64 { return Uint64.fromString(input.toString()); } +function decodeBaseAccount(data: Uint8Array, prefix: string): Account { + const { address, pubKey, accountNumber, sequence } = cosmos.auth.BaseAccount.decode(data); + // Pubkey is still Amino-encoded in BaseAccount (https://github.com/cosmos/cosmos-sdk/issues/6886) + const pubkey = pubKey.length ? decodeAminoPubkey(pubKey) : null; + return { + address: Bech32.encode(prefix, address), + pubkey: pubkey, + accountNumber: uint64FromProto(accountNumber).toNumber(), + sequence: uint64FromProto(sequence).toNumber(), + }; +} + function coinFromProto(input: cosmos.ICoin): Coin { assertDefined(input.amount); assertDefined(input.denom); @@ -41,23 +61,33 @@ export class StargateClient { this.tmClient = tmClient; } - public async getSequence(address: string): Promise { - const binAddress = Bech32.decode(address).data; + public async getAccount(searchAddress: string): Promise { + const { prefix, data: binAddress } = Bech32.decode(searchAddress); // https://github.com/cosmos/cosmos-sdk/blob/8cab43c8120fec5200c3459cbf4a92017bb6f287/x/auth/types/keys.go#L29-L32 const accountKey = Uint8Array.from([0x01, ...binAddress]); const responseData = await this.queryVerified("acc", accountKey); + if (responseData.length === 0) return null; + const { typeUrl, value } = decodeAny(responseData); switch (typeUrl) { case "/cosmos.auth.BaseAccount": { - const { accountNumber, sequence } = cosmos.auth.BaseAccount.decode(value); - return { - accountNumber: uint64FromProto(accountNumber).toNumber(), - sequence: uint64FromProto(sequence).toNumber(), - }; + return decodeBaseAccount(value, prefix); } default: - throw new Error(`Unsupported type: ${typeUrl}`); + throw new Error(`Unsupported type: '${typeUrl}'`); + } + } + + public async getSequence(address: string): Promise { + const account = await this.getAccount(address); + if (account) { + return { + accountNumber: account.accountNumber, + sequence: account.sequence, + }; + } else { + return null; } } diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index 5570c79b..c85aed67 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -24,4 +24,16 @@ export const unused = { balanceFee: "1000000000", // 1000 COSM }; +export const validator = { + /** From first gentx's auth_info.signer_infos in scripts/simapp/template/.simapp/config/genesis.json */ + pubkey: { + type: "tendermint/PubKeySecp256k1", + value: "AnFadRAdh6Fl7robHe8jywDMKSWQQjB7SlpoqGsX9Ghw", + }, + /** delegator_address from /cosmos.staking.MsgCreateValidator in scripts/simapp/template/.simapp/config/genesis.json */ + address: "cosmos12gm9sa666hywxu9nzzmp7hyl7a55hvg769w2kz", + accountNumber: 0, + sequence: 1, +}; + export const nonExistentAddress = "cosmos1p79apjaufyphcmsn4g07cynqf0wyjuezqu84hd"; diff --git a/packages/stargate/types/stargateclient.d.ts b/packages/stargate/types/stargateclient.d.ts index 0503db45..d0ea02ca 100644 --- a/packages/stargate/types/stargateclient.d.ts +++ b/packages/stargate/types/stargateclient.d.ts @@ -1,5 +1,12 @@ -import { Coin } from "@cosmjs/launchpad"; -export interface GetSequenceResult { +import { Coin, PubKey } from "@cosmjs/launchpad"; +export interface Account { + /** Bech32 account address */ + readonly address: string; + readonly pubkey: PubKey | null; + readonly accountNumber: number; + readonly sequence: number; +} +export interface SequenceResponse { readonly accountNumber: number; readonly sequence: number; } @@ -7,7 +14,8 @@ export declare class StargateClient { private readonly tmClient; static connect(endpoint: string): Promise; private constructor(); - getSequence(address: string): Promise; + getAccount(searchAddress: string): Promise; + getSequence(address: string): Promise; getBalance(address: string, searchDenom: string): Promise; /** * Queries all balances for all denoms that belong to this address.