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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -39,6 +39,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

* (x/bank) [\#11417](https://github.com/cosmos/cosmos-sdk/pull/11417) Introduce a new `SpendableBalances` gRPC query that retrieves an account's total (paginated) spendable balances.
* [\#11441](https://github.com/cosmos/cosmos-sdk/pull/11441) Added a new method, `IsLTE`, for `types.Coin`. This method is used to check if a `types.Coin` is less than or equal to another `types.Coin`.
* (x/upgrade) [\#11116](https://github.com/cosmos/cosmos-sdk/pull/11116) `MsgSoftwareUpgrade` and has been added to support v1beta2 msgs-based gov proposals.
* [\#11308](https://github.com/cosmos/cosmos-sdk/pull/11308) Added a mandatory metadata field to Vote in x/gov v1beta2.
Expand Down
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
36 changes: 36 additions & 0 deletions x/bank/keeper/grpc_query.go
Expand Up @@ -75,6 +75,42 @@ 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
}

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)
zeroAmt := sdk.ZeroInt()

pageRes, err := query.Paginate(accountStore, req.Pagination, func(key, value []byte) error {
balances = append(balances, sdk.NewCoin(string(key), zeroAmt))
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
53 changes: 53 additions & 0 deletions x/bank/keeper/grpc_query_test.go
Expand Up @@ -3,11 +3,14 @@ package keeper_test
import (
gocontext "context"
"fmt"
"time"

"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
"github.com/cosmos/cosmos-sdk/x/bank/testutil"
"github.com/cosmos/cosmos-sdk/x/bank/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
Expand Down Expand Up @@ -86,6 +89,56 @@ func (suite *IntegrationTestSuite) TestQueryAllBalances() {
suite.Nil(res.Pagination.NextKey)
}

func (suite *IntegrationTestSuite) TestSpendableBalances() {
app, ctx, queryClient := suite.app, suite.ctx, suite.queryClient
_, _, addr := testdata.KeyTestPubAddr()
ctx = ctx.WithBlockTime(time.Now())

_, err := queryClient.SpendableBalances(sdk.WrapSDKContext(ctx), &types.QuerySpendableBalancesRequest{})
suite.Require().Error(err)

pageReq := &query.PageRequest{
Key: nil,
Limit: 2,
CountTotal: false,
}
req := types.NewQuerySpendableBalancesRequest(addr, pageReq)

res, err := queryClient.SpendableBalances(sdk.WrapSDKContext(ctx), req)
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.True(res.Balances.IsZero())

fooCoins := newFooCoin(50)
barCoins := newBarCoin(30)

origCoins := sdk.NewCoins(fooCoins, barCoins)
acc := app.AccountKeeper.NewAccountWithAddress(ctx, addr)
acc = vestingtypes.NewContinuousVestingAccount(
acc.(*authtypes.BaseAccount),
sdk.NewCoins(fooCoins),
ctx.BlockTime().Unix(),
ctx.BlockTime().Add(time.Hour).Unix(),
)

app.AccountKeeper.SetAccount(ctx, acc)
suite.Require().NoError(testutil.FundAccount(app.BankKeeper, ctx, acc.GetAddress(), origCoins))

// move time forward for some tokens to vest
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(30 * time.Minute))
queryHelper := baseapp.NewQueryServerTestHelper(ctx, app.InterfaceRegistry())
types.RegisterQueryServer(queryHelper, app.BankKeeper)
queryClient = types.NewQueryClient(queryHelper)

res, err = queryClient.SpendableBalances(sdk.WrapSDKContext(ctx), req)
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Equal(2, res.Balances.Len())
suite.Nil(res.Pagination.NextKey)
suite.EqualValues(30, res.Balances[0].Amount.Int64())
suite.EqualValues(25, res.Balances[1].Amount.Int64())
}

func (suite *IntegrationTestSuite) TestQueryTotalSupply() {
app, ctx, queryClient := suite.app, suite.ctx, suite.queryClient
res, err := queryClient.TotalSupply(gocontext.Background(), &types.QueryTotalSupplyRequest{})
Expand Down
2 changes: 1 addition & 1 deletion x/bank/keeper/keeper_test.go
Expand Up @@ -95,7 +95,7 @@ func (suite *IntegrationTestSuite) initKeepersWithmAccPerms(blockedAddrs map[str

func (suite *IntegrationTestSuite) SetupTest() {
app := simapp.Setup(suite.T(), false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})
ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Now()})

app.AccountKeeper.SetParams(ctx, authtypes.DefaultParams())
app.BankKeeper.SetParams(ctx, types.DefaultParams())
Expand Down
7 changes: 7 additions & 0 deletions x/bank/types/querier.go
Expand Up @@ -25,6 +25,13 @@ func NewQueryAllBalancesRequest(addr sdk.AccAddress, req *query.PageRequest) *Qu
return &QueryAllBalancesRequest{Address: addr.String(), Pagination: req}
}

// NewQuerySpendableBalancesRequest creates a new instance of a
// QuerySpendableBalancesRequest.
// nolint:interfacer
func NewQuerySpendableBalancesRequest(addr sdk.AccAddress, req *query.PageRequest) *QuerySpendableBalancesRequest {
return &QuerySpendableBalancesRequest{Address: addr.String(), Pagination: req}
}

// QueryTotalSupplyParams defines the params for the following queries:
//
// - 'custom/bank/totalSupply'
Expand Down