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: (x/bank) add spendable balances cmd (backport #14045) #14514

Merged
merged 2 commits into from
Jan 5, 2023
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

* (x/bank) [#14045](https://github.com/cosmos/cosmos-sdk/pull/14045) Add CLI command `spendable-balances`, which also accepts the flag `--denom`.
* (x/slashing, x/staking) [#14363](https://github.com/cosmos/cosmos-sdk/pull/14363) Add the infraction a validator commited type as an argument to a `SlashWithInfractionReason` keeper method.
* (client) [#14051](https://github.com/cosmos/cosmos-sdk/pull/14051) Add `--grpc` client option.
* (x/genutil) [#14149](https://github.com/cosmos/cosmos-sdk/pull/14149) Add `genutilcli.GenesisCoreCommand` command, which contains all genesis-related sub-commands.
Expand Down
1,754 changes: 1,409 additions & 345 deletions api/cosmos/bank/v1beta1/query.pulsar.go

Large diffs are not rendered by default.

54 changes: 52 additions & 2 deletions api/cosmos/bank/v1beta1/query_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 38 additions & 1 deletion proto/cosmos/bank/v1beta1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ service Query {
option (google.api.http).get = "/cosmos/bank/v1beta1/balances/{address}";
}

// SpendableBalances queries the spenable balance of all coins for a single
// SpendableBalances queries the spendable balance of all coins for a single
// account.
//
// When called from another module, this query might consume a high amount of
Expand All @@ -41,6 +41,18 @@ service Query {
option (google.api.http).get = "/cosmos/bank/v1beta1/spendable_balances/{address}";
}

// SpendableBalanceByDenom queries the spendable balance of a single denom for
// a single account.
//
// When called from another module, this query might consume a high amount of
// gas if the pagination field is incorrectly set.
//
// Since: cosmos-sdk 0.47
rpc SpendableBalanceByDenom(QuerySpendableBalanceByDenomRequest) returns (QuerySpendableBalanceByDenomResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/cosmos/bank/v1beta1/spendable_balances/{address}/by_denom";
}

// TotalSupply queries the total supply of all coins.
//
// When called from another module, this query might consume a high amount of
Expand Down Expand Up @@ -178,6 +190,31 @@ message QuerySpendableBalancesResponse {
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

// QuerySpendableBalanceByDenomRequest defines the gRPC request structure for
// querying an account's spendable balance for a specific denom.
//
// Since: cosmos-sdk 0.47
message QuerySpendableBalanceByDenomRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

// address is the address to query balances for.
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];

// denom is the coin denom to query balances for.
string denom = 2;
}

// QuerySpendableBalanceByDenomResponse defines the gRPC response structure for
// querying an account's spendable balance for a specific denom.
//
// Since: cosmos-sdk 0.47
message QuerySpendableBalanceByDenomResponse {
// balance is the balance of the coin.
cosmos.base.v1beta1.Coin balance = 1;
}


// QueryTotalSupplyRequest is the request type for the Query/TotalSupply RPC
// method.
message QueryTotalSupplyRequest {
Expand Down
61 changes: 61 additions & 0 deletions x/bank/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func GetQueryCmd() *cobra.Command {

cmd.AddCommand(
GetBalancesCmd(),
GetSpendableBalancesCmd(),
GetCmdQueryTotalSupply(),
GetCmdDenomsMetadata(),
GetCmdQuerySendEnabled(),
Expand Down Expand Up @@ -108,6 +109,66 @@ Example:
return cmd
}

func GetSpendableBalancesCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "spendable-balances [address]",
Short: "Query for account spendable balances by address",
Example: fmt.Sprintf("$ %s query %s spendable-balances [address]", version.AppName, types.ModuleName),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

denom, err := cmd.Flags().GetString(FlagDenom)
if err != nil {
return err
}

queryClient := types.NewQueryClient(clientCtx)

addr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}

pageReq, err := client.ReadPageRequest(cmd.Flags())
if err != nil {
return err
}

ctx := cmd.Context()

if denom == "" {
params := types.NewQuerySpendableBalancesRequest(addr, pageReq)

res, err := queryClient.SpendableBalances(ctx, params)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
}

params := types.NewQuerySpendableBalanceByDenomRequest(addr, denom)

res, err := queryClient.SpendableBalanceByDenom(ctx, params)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

cmd.Flags().String(FlagDenom, "", "The specific balance denomination to query for")
flags.AddQueryFlagsToCmd(cmd)
flags.AddPaginationFlagsToCmd(cmd, "spendable balances")

return cmd
}

// GetCmdDenomsMetadata defines the cobra command to query client denomination metadata.
func GetCmdDenomsMetadata() *cobra.Command {
cmd := &cobra.Command{
Expand Down
84 changes: 84 additions & 0 deletions x/bank/client/cli/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,90 @@ func (s *CLITestSuite) TestGetBalancesCmd() {
}
}

func (s *CLITestSuite) TestGetSpendableBalancesCmd() {
accounts := testutil.CreateKeyringAccounts(s.T(), s.kr, 1)

cmd := cli.GetSpendableBalancesCmd()
cmd.SetOutput(io.Discard)

testCases := []struct {
name string
ctxGen func() client.Context
args []string
expectResult proto.Message
expectErr bool
}{
{
"valid query",
func() client.Context {
bz, _ := s.encCfg.Codec.Marshal(&types.QuerySpendableBalancesResponse{})
c := clitestutil.NewMockTendermintRPC(abci.ResponseQuery{
Value: bz,
})
return s.baseCtx.WithClient(c)
},
[]string{
accounts[0].Address.String(),
fmt.Sprintf("--%s=json", flags.FlagOutput),
},
&types.QuerySpendableBalancesResponse{},
false,
},
{
"valid query with denom flag",
func() client.Context {
bz, _ := s.encCfg.Codec.Marshal(&types.QuerySpendableBalanceByDenomRequest{})
c := clitestutil.NewMockTendermintRPC(abci.ResponseQuery{
Value: bz,
})
return s.baseCtx.WithClient(c)
},
[]string{
accounts[0].Address.String(),
fmt.Sprintf("--%s=json", flags.FlagOutput),
fmt.Sprintf("--%s=photon", cli.FlagDenom),
},
&types.QuerySpendableBalanceByDenomResponse{},
false,
},
{
"invalid Address",
func() client.Context {
return s.baseCtx
},
[]string{
"foo",
},
nil,
true,
},
}

for _, tc := range testCases {
tc := tc

s.Run(tc.name, func() {
var outBuf bytes.Buffer

clientCtx := tc.ctxGen().WithOutput(&outBuf)
ctx := svrcmd.CreateExecuteContext(context.Background())

cmd.SetContext(ctx)
cmd.SetArgs(tc.args)

s.Require().NoError(client.SetCmdClientContextHandler(clientCtx, cmd))

err := cmd.Execute()
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(s.encCfg.Codec.UnmarshalJSON(outBuf.Bytes(), tc.expectResult))
s.Require().NoError(err)
}
})
}
}

func (s *CLITestSuite) TestGetCmdDenomsMetadata() {
cmd := cli.GetCmdDenomsMetadata()
cmd.SetOutput(io.Discard)
Expand Down
31 changes: 23 additions & 8 deletions x/bank/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ func (k BaseKeeper) Balance(ctx context.Context, req *types.QueryBalanceRequest)
return nil, status.Error(codes.InvalidArgument, "empty request")
}

if req.Address == "" {
return nil, status.Error(codes.InvalidArgument, "address cannot be empty")
}

if req.Denom == "" {
return nil, status.Error(codes.InvalidArgument, "invalid denom")
}
Expand All @@ -47,10 +43,6 @@ func (k BaseKeeper) AllBalances(ctx context.Context, req *types.QueryAllBalances
return nil, status.Error(codes.InvalidArgument, "empty request")
}

if req.Address == "" {
return nil, status.Error(codes.InvalidArgument, "address cannot be empty")
}

addr, err := sdk.AccAddressFromBech32(req.Address)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid address: %s", err.Error())
Expand Down Expand Up @@ -113,6 +105,29 @@ func (k BaseKeeper) SpendableBalances(ctx context.Context, req *types.QuerySpend
return &types.QuerySpendableBalancesResponse{Balances: result, Pagination: pageRes}, nil
}

// SpendableBalanceByDenom implements a gRPC query handler for retrieving an account's
// spendable balance for a specific denom.
func (k BaseKeeper) SpendableBalanceByDenom(ctx context.Context, req *types.QuerySpendableBalanceByDenomRequest) (*types.QuerySpendableBalanceByDenomResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

addr, err := sdk.AccAddressFromBech32(req.Address)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid address: %s", err.Error())
}

if req.Denom == "" {
return nil, status.Error(codes.InvalidArgument, "invalid denom")
}

sdkCtx := sdk.UnwrapSDKContext(ctx)

spendable := k.SpendableCoin(sdkCtx, addr, req.Denom)

return &types.QuerySpendableBalanceByDenomResponse{Balance: &spendable}, nil
}

// TotalSupply implements the Query/TotalSupply gRPC method
func (k BaseKeeper) TotalSupply(ctx context.Context, req *types.QueryTotalSupplyRequest) (*types.QueryTotalSupplyResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
Expand Down
Loading