Merge pull request #355 from CosmWasm/balance-queries

Add getUnverifiedAllBalances
This commit is contained in:
mergify[bot] 2020-08-06 16:17:54 +00:00 committed by GitHub
commit 0cb2c9fae7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 158 additions and 22 deletions

View File

@ -1,3 +1,3 @@
export { BaseAccount } from "./accounts";
export { decodeAny } from "./any";
export { Coin } from "./msgs";
export { cosmosField } from "./decorator";

View File

@ -1,3 +1,3 @@
export { BaseAccount } from "./accounts";
export { decodeAny } from "./any";
export { Coin } from "./msgs";
export { cosmosField } from "./decorator";

View File

@ -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",

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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([]);
});
});
});

View File

@ -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<Coin | null> {
// 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<readonly Coin[]> {
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<Uint8Array> {
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;
}
}

View File

@ -23,3 +23,5 @@ export const unused = {
balanceStaking: "10000000", // 10 STAKE
balanceFee: "1000000000", // 1000 COSM
};
export const nonExistentAddress = "cosmos1p79apjaufyphcmsn4g07cynqf0wyjuezqu84hd";

View File

@ -4,6 +4,7 @@
"baseUrl": ".",
"outDir": "build",
"declarationDir": "build/types",
"experimentalDecorators": true,
"rootDir": "src"
},
"include": [

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
/// <reference types="long" />
import { Message } from "protobufjs";
export declare class PageRequest extends Message {}
export declare class PageResponse extends Message {
readonly nextKey?: Uint8Array;
readonly total?: Long | number;
}

View File

@ -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<StargateClient>;
private constructor();
getSequence(address: string): Promise<GetSequenceResult>;
getBalance(
address: string,
searchDenom: string,
): Promise<{
readonly denom: string;
readonly amount: string;
} | null>;
getBalance(address: string, searchDenom: string): Promise<Coin | null>;
/**
* 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<readonly Coin[]>;
disconnect(): void;
private queryVerified;
private queryUnverified;
}