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: add support for spendable balances gRPC query #11417

Merged
merged 13 commits into from Mar 25, 2022
30 changes: 30 additions & 0 deletions proto/cosmos/bank/v1beta1/query.proto
Expand Up @@ -22,6 +22,12 @@ service Query {
option (google.api.http).get = "/cosmos/bank/v1beta1/balances/{address}";
}

// SpendableBalances queries the spenable balance of all coins for a single
// account.
rpc SpendableBalances(QuerySpendableBalancesRequest) returns (QuerySpendableBalancesResponse) {
option (google.api.http).get = "/cosmos/bank/v1beta1/spendable_balances/{address}";
}

// TotalSupply queries the total supply of all coins.
rpc TotalSupply(QueryTotalSupplyRequest) returns (QueryTotalSupplyResponse) {
option (google.api.http).get = "/cosmos/bank/v1beta1/supply";
Expand Down Expand Up @@ -96,6 +102,30 @@ message QueryAllBalancesResponse {
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

// QuerySpendableBalancesRequest defines the gRPC request structure for querying
// an account's spendable balances.
message QuerySpendableBalancesRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

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

// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}

// QuerySpendableBalancesResponse defines the gRPC response structure for querying
// an account's spendable balances.
message QuerySpendableBalancesResponse {
// balances is the spendable balances of all the coins.
repeated cosmos.base.v1beta1.Coin balances = 1
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];

// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

// QueryTotalSupplyRequest is the request type for the Query/TotalSupply RPC
// method.
message QueryTotalSupplyRequest {
Expand Down
42 changes: 42 additions & 0 deletions x/bank/keeper/grpc_query.go
Expand Up @@ -75,6 +75,48 @@ func (k BaseKeeper) AllBalances(ctx context.Context, req *types.QueryAllBalances
return &types.QueryAllBalancesResponse{Balances: balances, Pagination: pageRes}, nil
}

// SpendableBalances implements a gRPC query handler for retrieving an account's
// spendable balances.
func (k BaseKeeper) SpendableBalances(ctx context.Context, req *types.QuerySpendableBalancesRequest) (*types.QuerySpendableBalancesResponse, error) {
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
}
if req.Address == "" {
return nil, status.Error(codes.InvalidArgument, "address cannot be empty")
}
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved

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

sdkCtx := sdk.UnwrapSDKContext(ctx)

balances := sdk.NewCoins()
accountStore := k.getAccountStore(sdkCtx, addr)

pageRes, err := query.Paginate(accountStore, req.Pagination, func(key, value []byte) error {
var amount sdk.Int
if err := amount.Unmarshal(value); err != nil {
return err
}
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
balances = append(balances, sdk.NewCoin(string(key), amount))
return nil
})
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "paginate: %v", err)
}

result := sdk.NewCoins()
spendable := k.SpendableCoins(sdkCtx, addr)
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved

for _, c := range balances {
result = append(result, sdk.NewCoin(c.Denom, spendable.AmountOf(c.Denom)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function can be called by other transaction. So, let's charge extra gas here. Eg 20gas per iteration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Charge gas? This is a query, not a transaction.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know. But it can be called from a transaction, in other words, a RPC Msg method, when running can do a query call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow?

These methods are for queries only. In fact, in a subsequent PR, I'll be removing these from the Keeper entirely, in favor of a dedicated Querier (Example). All modules should follow this approach.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at this example: https://github.com/regen-network/regen-ledger/blob/master/x/ecocredit/server/msg_server.go#L718 - The rpc Buy will call bank.SpendableCoins.

Technically we can create a QueryClient of other module, and use it when processing a transaction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically we can create a QueryClient of other module, and use it when processing a transaction.

That doesn't make any sense. Gas and txs are not involved here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably I didn't express myself well enough. Here is the snippet (didn't test it but I believe it's possible):

/// in authz Grant Msg function
// let's imagine a scenario where we want to check spendable coins
func (k Keeper) Grant(goCtx context.Context, msg *authz.MsgGrant) (*authz.MsgGrantResponse, error) {
   ...  
   bc := banktypes.NewQueryClient(ctx)
   resp, err := bc.SpendableBalances(ctx, types.QuerySpendableBalancesRequest{...})
   ....

BTW - I think this is general discussion if we should check if we should consider checking if context provides a gas meter in queries and charge gas (so feel free to merge)

}

return &types.QuerySpendableBalancesResponse{Balances: result, Pagination: pageRes}, 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