Skip to content

Commit

Permalink
feat: (x/bank) add spendable balances cmd (#14045)
Browse files Browse the repository at this point in the history
  • Loading branch information
facundomedica committed Jan 5, 2023
1 parent 5d62366 commit 6ac0c36
Show file tree
Hide file tree
Showing 12 changed files with 2,402 additions and 445 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ 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) [#13867](https://github.com/cosmos/cosmos-sdk/pull/13867/) Wire AutoCLI commands with SimApp.
* (x/distribution) [#14322](https://github.com/cosmos/cosmos-sdk/pull/14322) Introduce a new gRPC message handler, `DepositValidatorRewardsPool`, that allows explicit funding of a validator's reward pool.
* (x/slashing, x/staking) [#14363](https://github.com/cosmos/cosmos-sdk/pull/14363) Add the infraction a validator committed type as an argument to a `SlashWithInfractionReason` keeper method.
* (x/evidence) [#13740](https://github.com/cosmos/cosmos-sdk/pull/13740) Add new proto field `hash` of type `string` to `QueryEvidenceRequest` which helps to decode the hash properly while using query API.
* (core) [#13306](https://github.com/cosmos/cosmos-sdk/pull/13306) Add a `FormatCoins` function to in `core/coins` to format sdk Coins following the Value Renderers spec.
* (math) [#13306](https://github.com/cosmos/cosmos-sdk/pull/13306) Add `FormatInt` and `FormatDec` functions in `math` to format integers and decimals following the Value Renderers spec.
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

0 comments on commit 6ac0c36

Please sign in to comment.