diff --git a/packages/stargate/src/queries/auth.spec.ts b/packages/stargate/src/queries/auth.spec.ts new file mode 100644 index 00000000..4befcb65 --- /dev/null +++ b/packages/stargate/src/queries/auth.spec.ts @@ -0,0 +1,96 @@ +import { encodeAminoPubkey } from "@cosmjs/launchpad"; +import { Client as TendermintClient } from "@cosmjs/tendermint-rpc"; +import { assert } from "@cosmjs/utils"; +import Long from "long"; + +import { nonExistentAddress, pendingWithoutSimapp, simapp, unused, validator } from "../testutils.spec"; +import { AuthExtension, setupAuthExtension } from "./auth"; +import { QueryClient } from "./queryclient"; +import { toAccAddress } from "./utils"; + +async function makeAuthClient(rpcUrl: string): Promise { + // TODO: tmClient is not owned by QueryClient but should be disconnected somehow (once we use WebSockets) + const tmClient = await TendermintClient.connect(rpcUrl); + return QueryClient.withExtensions(tmClient, setupAuthExtension); +} + +describe("AuthExtension", () => { + describe("account", () => { + it("works for unused account", async () => { + pendingWithoutSimapp(); + const client = await makeAuthClient(simapp.tendermintUrl); + + const account = await client.auth.account(unused.address); + assert(account); + expect(account).toEqual({ + address: toAccAddress(unused.address), + // pubKey not set + accountNumber: Long.fromNumber(unused.accountNumber, true), + // sequence not set + }); + }); + + it("works for account with pubkey and non-zero sequence", async () => { + pendingWithoutSimapp(); + const client = await makeAuthClient(simapp.tendermintUrl); + + const account = await client.auth.account(validator.address); + assert(account); + expect(account).toEqual({ + address: toAccAddress(validator.address), + pubKey: encodeAminoPubkey(validator.pubkey), + // accountNumber not set + sequence: Long.fromNumber(validator.sequence, true), + }); + }); + + it("returns null for non-existent address", async () => { + pendingWithoutSimapp(); + const client = await makeAuthClient(simapp.tendermintUrl); + + const account = await client.auth.account(nonExistentAddress); + expect(account).toBeNull(); + }); + }); + + describe("unverified", () => { + describe("account", () => { + it("works for unused account", async () => { + pendingWithoutSimapp(); + const client = await makeAuthClient(simapp.tendermintUrl); + + const account = await client.auth.unverified.account(unused.address); + assert(account); + expect(account).toEqual({ + address: toAccAddress(unused.address), + // pubKey not set + accountNumber: Long.fromNumber(unused.accountNumber, true), + // sequence not set + }); + }); + + it("works for account with pubkey and non-zero sequence", async () => { + pendingWithoutSimapp(); + const client = await makeAuthClient(simapp.tendermintUrl); + + const account = await client.auth.unverified.account(validator.address); + assert(account); + expect(account).toEqual({ + address: toAccAddress(validator.address), + pubKey: encodeAminoPubkey(validator.pubkey), + // accountNumber not set + sequence: Long.fromNumber(validator.sequence, true), + }); + }); + + it("returns null for non-existent address", async () => { + pending("This fails with Error: Query failed with (1): internal"); + pendingWithoutSimapp(); + const client = await makeAuthClient(simapp.tendermintUrl); + + const account = await client.auth.unverified.account(nonExistentAddress); + expect(account).toBeNull(); + }); + }); + }); +}); diff --git a/packages/stargate/src/queries/auth.ts b/packages/stargate/src/queries/auth.ts new file mode 100644 index 00000000..fbc4c470 --- /dev/null +++ b/packages/stargate/src/queries/auth.ts @@ -0,0 +1,60 @@ +import { assert } from "@cosmjs/utils"; + +import { cosmos, google } from "../generated/codecimpl"; +import { QueryClient } from "./queryclient"; +import { toAccAddress, toObject } from "./utils"; + +export interface AuthExtension { + readonly auth: { + readonly account: (address: string) => Promise; + readonly unverified: { + readonly account: (address: string) => Promise; + }; + }; +} + +export function setupAuthExtension(base: QueryClient): AuthExtension { + // Use this service to get easy typed access to query methods + // This cannot be used to for proof verification + const queryService = cosmos.auth.Query.create((method: any, requestData, callback) => { + // Parts of the path are unavailable, so we hardcode them here. See https://github.com/protobufjs/protobuf.js/issues/1229 + const path = `/cosmos.auth.Query/${method.name}`; + base + .queryUnverified(path, requestData) + .then((response) => callback(null, response)) + .catch((error) => callback(error)); + }); + + return { + auth: { + account: async (address: string) => { + // https://github.com/cosmos/cosmos-sdk/blob/8cab43c8120fec5200c3459cbf4a92017bb6f287/x/auth/types/keys.go#L29-L32 + const key = Uint8Array.from([0x01, ...toAccAddress(address)]); + const responseData = await base.queryVerified("acc", key); + if (responseData.length === 0) return null; + const account = google.protobuf.Any.decode(responseData); + switch (account.type_url) { + case "/cosmos.auth.BaseAccount": { + return toObject(cosmos.auth.BaseAccount.decode(account.value)); + } + default: + throw new Error(`Unsupported type: '${account.type_url}'`); + } + }, + unverified: { + account: async (address: string) => { + const { account } = await queryService.account({ address: toAccAddress(address) }); + if (!account) return null; + switch (account.type_url) { + case "/cosmos.auth.BaseAccount": { + assert(account.value); + return toObject(cosmos.auth.BaseAccount.decode(account.value)); + } + default: + throw new Error(`Unsupported type: '${account.type_url}'`); + } + }, + }, + }, + }; +} diff --git a/packages/stargate/src/queries/index.ts b/packages/stargate/src/queries/index.ts index 854d741c..afcfb022 100644 --- a/packages/stargate/src/queries/index.ts +++ b/packages/stargate/src/queries/index.ts @@ -4,4 +4,5 @@ export { QueryClient } from "./queryclient"; // Extensions +export { AuthExtension, setupAuthExtension } from "./auth"; export { BankExtension, setupBankExtension } from "./bank"; diff --git a/packages/stargate/types/queries/auth.d.ts b/packages/stargate/types/queries/auth.d.ts new file mode 100644 index 00000000..db6bcb4f --- /dev/null +++ b/packages/stargate/types/queries/auth.d.ts @@ -0,0 +1,11 @@ +import { cosmos } from "../generated/codecimpl"; +import { QueryClient } from "./queryclient"; +export interface AuthExtension { + readonly auth: { + readonly account: (address: string) => Promise; + readonly unverified: { + readonly account: (address: string) => Promise; + }; + }; +} +export declare function setupAuthExtension(base: QueryClient): AuthExtension; diff --git a/packages/stargate/types/queries/index.d.ts b/packages/stargate/types/queries/index.d.ts index 1e80a28e..3aacc3e3 100644 --- a/packages/stargate/types/queries/index.d.ts +++ b/packages/stargate/types/queries/index.d.ts @@ -1,2 +1,3 @@ export { QueryClient } from "./queryclient"; +export { AuthExtension, setupAuthExtension } from "./auth"; export { BankExtension, setupBankExtension } from "./bank";