diff --git a/packages/proto-signing/src/index.ts b/packages/proto-signing/src/index.ts index c52b350f..28e7bc70 100644 --- a/packages/proto-signing/src/index.ts +++ b/packages/proto-signing/src/index.ts @@ -1,3 +1,3 @@ -export { BaseAccount } from "./accounts"; export { decodeAny } from "./any"; export { Coin } from "./msgs"; +export { cosmosField } from "./decorator"; diff --git a/packages/proto-signing/types/index.d.ts b/packages/proto-signing/types/index.d.ts index c52b350f..28e7bc70 100644 --- a/packages/proto-signing/types/index.d.ts +++ b/packages/proto-signing/types/index.d.ts @@ -1,3 +1,3 @@ -export { BaseAccount } from "./accounts"; export { decodeAny } from "./any"; export { Coin } from "./msgs"; +export { cosmosField } from "./decorator"; diff --git a/packages/stargate/package.json b/packages/stargate/package.json index 647c715b..0617276f 100644 --- a/packages/stargate/package.json +++ b/packages/stargate/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@cosmjs/encoding": "^0.22.0", + "@cosmjs/launchpad": "^0.22.0", "@cosmjs/math": "^0.22.0", "@cosmjs/proto-signing": "^0.22.0", "@cosmjs/tendermint-rpc": "^0.22.0", diff --git a/packages/proto-signing/src/accounts.ts b/packages/stargate/src/query/accounts.ts similarity index 88% rename from packages/proto-signing/src/accounts.ts rename to packages/stargate/src/query/accounts.ts index c13d6728..8c48497b 100644 --- a/packages/proto-signing/src/accounts.ts +++ b/packages/stargate/src/query/accounts.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { cosmosField } from "@cosmjs/proto-signing"; import { Message } from "protobufjs"; -import { cosmosField } from "./decorator"; - export class BaseAccount extends Message { @cosmosField.bytes(1) public readonly address?: Uint8Array; diff --git a/packages/stargate/src/query/allbalances.ts b/packages/stargate/src/query/allbalances.ts new file mode 100644 index 00000000..396aeb12 --- /dev/null +++ b/packages/stargate/src/query/allbalances.ts @@ -0,0 +1,23 @@ +import { Coin, cosmosField } from "@cosmjs/proto-signing"; +import { Message } from "protobufjs"; + +import { PageRequest, PageResponse } from "./pagination"; + +// these grpc query types come from: +// https://github.com/cosmos/cosmos-sdk/blob/69bbb8b327c3cfb967d969bcadeb9b0aef144df6/proto/cosmos/bank/query.proto#L40-L55 + +export class QueryAllBalancesRequest extends Message { + @cosmosField.bytes(1) + public readonly address?: Uint8Array; + + @cosmosField.message(2, PageRequest) + public readonly pagination?: PageRequest; +} + +export class QueryAllBalancesResponse extends Message { + @cosmosField.repeatedMessage(1, Coin) + public readonly balances?: readonly Coin[]; + + @cosmosField.message(2, PageResponse) + public readonly pagination?: PageResponse; +} diff --git a/packages/stargate/src/query/pagination.ts b/packages/stargate/src/query/pagination.ts new file mode 100644 index 00000000..2ffca347 --- /dev/null +++ b/packages/stargate/src/query/pagination.ts @@ -0,0 +1,18 @@ +import { cosmosField } from "@cosmjs/proto-signing"; +import { Message } from "protobufjs"; + +export class PageRequest extends Message { + // TODO: implement +} + +export class PageResponse extends Message { + // next_key is the key to be passed to PageRequest.key to + // query the next page most efficiently + @cosmosField.bytes(1) + public readonly nextKey?: Uint8Array; + + // total is total number of results available if PageRequest.count_total + // was set, its value is undefined otherwise + @cosmosField.uint64(2) + public readonly total?: Long | number; +} diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index a376253c..484a1eb8 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -1,5 +1,5 @@ import { StargateClient } from "./stargateclient"; -import { pendingWithoutSimapp, simapp, unused } from "./testutils.spec"; +import { nonExistentAddress, pendingWithoutSimapp, simapp, unused } from "./testutils.spec"; describe("StargateClient", () => { describe("connect", () => { @@ -52,5 +52,41 @@ describe("StargateClient", () => { client.disconnect(); }); + + it("returns null for non-existent address", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + + const response = await client.getBalance(nonExistentAddress, simapp.denomFee); + expect(response).toBeNull(); + + client.disconnect(); + }); + }); + + describe("getAllBalancesUnverified", () => { + it("returns all balances for unused account", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + + const balances = await client.getAllBalancesUnverified(unused.address); + expect(balances).toEqual([ + { + amount: unused.balanceFee, + denom: simapp.denomFee, + }, + { + amount: unused.balanceStaking, + denom: simapp.denomStaking, + }, + ]); + }); + + it("returns an empty list for non-existent account", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + const balances = await client.getAllBalancesUnverified(nonExistentAddress); + expect(balances).toEqual([]); + }); }); }); diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index ec23473c..4c9094f7 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -1,11 +1,15 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Bech32, toAscii, toHex } from "@cosmjs/encoding"; +import { Coin } from "@cosmjs/launchpad"; import { Uint64 } from "@cosmjs/math"; -import { BaseAccount, Coin, decodeAny } from "@cosmjs/proto-signing"; +import * as proto from "@cosmjs/proto-signing"; import { Client as TendermintClient } from "@cosmjs/tendermint-rpc"; import { assertDefined } from "@cosmjs/utils"; import Long from "long"; +import { BaseAccount } from "./query/accounts"; +import { QueryAllBalancesRequest, QueryAllBalancesResponse } from "./query/allbalances"; + export interface GetSequenceResult { readonly accountNumber: number; readonly sequence: number; @@ -15,6 +19,15 @@ function uint64FromProto(input: number | Long): Uint64 { return Uint64.fromString(input.toString()); } +function coinFromProto(input: proto.Coin): Coin { + assertDefined(input.amount); + assertDefined(input.denom); + return { + amount: input.amount, + denom: input.denom, + }; +} + export class StargateClient { private readonly tmClient: TendermintClient; @@ -33,7 +46,7 @@ export class StargateClient { const accountKey = Uint8Array.from([0x01, ...binAddress]); const responseData = await this.queryVerified("acc", accountKey); - const { typeUrl, value } = decodeAny(responseData); + const { typeUrl, value } = proto.decodeAny(responseData); switch (typeUrl) { case "/cosmos.auth.BaseAccount": { const { account_number, sequence } = BaseAccount.decode(value); @@ -49,13 +62,7 @@ export class StargateClient { } } - public async getBalance( - address: string, - searchDenom: string, - ): Promise<{ - readonly denom: string; - readonly amount: string; - } | null> { + public async getBalance(address: string, searchDenom: string): Promise { // balance key is a bit tricker, using some prefix stores // https://github.com/cosmwasm/cosmos-sdk/blob/80f7ff62f79777a487d0c7a53c64b0f7e43c47b9/x/bank/keeper/view.go#L74-L77 // ("balances", binAddress, denom) @@ -66,7 +73,7 @@ export class StargateClient { const bankKey = Uint8Array.from([...toAscii("balances"), ...binAddress, ...toAscii(searchDenom)]); const responseData = await this.queryVerified("bank", bankKey); - const { amount, denom } = Coin.decode(responseData); + const { amount, denom } = proto.Coin.decode(responseData); assertDefined(amount); assertDefined(denom); @@ -80,6 +87,20 @@ export class StargateClient { } } + /** + * Queries all balances for all denoms that belong to this address. + * + * Uses the grpc queries (which iterates over the store internally), and we cannot get + * proofs from such a method. + */ + public async getAllBalancesUnverified(address: string): Promise { + const path = "/cosmos.bank.Query/AllBalances"; + const request = QueryAllBalancesRequest.encode({ address: Bech32.decode(address).data }).finish(); + const responseData = await this.queryUnverified(path, request); + const response = QueryAllBalancesResponse.decode(responseData); + return (response.balances || []).map(coinFromProto); + } + public disconnect(): void { this.tmClient.disconnect(); } @@ -107,4 +128,18 @@ export class StargateClient { return response.value; } + + private async queryUnverified(path: string, request: Uint8Array): Promise { + const response = await this.tmClient.abciQuery({ + path: path, + data: request, + prove: false, + }); + + if (response.code) { + throw new Error(`Query failed with (${response.code}): ${response.log}`); + } + + return response.value; + } } diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index 1abc4554..5570c79b 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -23,3 +23,5 @@ export const unused = { balanceStaking: "10000000", // 10 STAKE balanceFee: "1000000000", // 1000 COSM }; + +export const nonExistentAddress = "cosmos1p79apjaufyphcmsn4g07cynqf0wyjuezqu84hd"; diff --git a/packages/stargate/tsconfig.json b/packages/stargate/tsconfig.json index 167e8c02..c605e918 100644 --- a/packages/stargate/tsconfig.json +++ b/packages/stargate/tsconfig.json @@ -4,6 +4,7 @@ "baseUrl": ".", "outDir": "build", "declarationDir": "build/types", + "experimentalDecorators": true, "rootDir": "src" }, "include": [ diff --git a/packages/proto-signing/types/accounts.d.ts b/packages/stargate/types/query/accounts.d.ts similarity index 100% rename from packages/proto-signing/types/accounts.d.ts rename to packages/stargate/types/query/accounts.d.ts diff --git a/packages/stargate/types/query/allbalances.d.ts b/packages/stargate/types/query/allbalances.d.ts new file mode 100644 index 00000000..3e418fee --- /dev/null +++ b/packages/stargate/types/query/allbalances.d.ts @@ -0,0 +1,11 @@ +import { Coin } from "@cosmjs/proto-signing"; +import { Message } from "protobufjs"; +import { PageRequest, PageResponse } from "./pagination"; +export declare class QueryAllBalancesRequest extends Message { + readonly address?: Uint8Array; + readonly pagination?: PageRequest; +} +export declare class QueryAllBalancesResponse extends Message { + readonly balances?: readonly Coin[]; + readonly pagination?: PageResponse; +} diff --git a/packages/stargate/types/query/pagination.d.ts b/packages/stargate/types/query/pagination.d.ts new file mode 100644 index 00000000..472b205d --- /dev/null +++ b/packages/stargate/types/query/pagination.d.ts @@ -0,0 +1,7 @@ +/// +import { Message } from "protobufjs"; +export declare class PageRequest extends Message {} +export declare class PageResponse extends Message { + readonly nextKey?: Uint8Array; + readonly total?: Long | number; +} diff --git a/packages/stargate/types/stargateclient.d.ts b/packages/stargate/types/stargateclient.d.ts index 85cafca9..0503db45 100644 --- a/packages/stargate/types/stargateclient.d.ts +++ b/packages/stargate/types/stargateclient.d.ts @@ -1,3 +1,4 @@ +import { Coin } from "@cosmjs/launchpad"; export interface GetSequenceResult { readonly accountNumber: number; readonly sequence: number; @@ -7,13 +8,15 @@ export declare class StargateClient { static connect(endpoint: string): Promise; private constructor(); getSequence(address: string): Promise; - getBalance( - address: string, - searchDenom: string, - ): Promise<{ - readonly denom: string; - readonly amount: string; - } | null>; + getBalance(address: string, searchDenom: string): Promise; + /** + * Queries all balances for all denoms that belong to this address. + * + * Uses the grpc queries (which iterates over the store internally), and we cannot get + * proofs from such a method. + */ + getAllBalancesUnverified(address: string): Promise; disconnect(): void; private queryVerified; + private queryUnverified; }