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 (backport #11417) #11459

Merged
merged 6 commits into from
Mar 27, 2022
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.
Jump to
Jump to file
Failed to load files.
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) [\#11417](https://github.com/cosmos/cosmos-sdk/pull/11417) Introduce a new `SpendableBalances` gRPC query that retrieves an account's total (paginated) spendable balances.
* (x/bank) [\#10771](https://github.com/cosmos/cosmos-sdk/pull/10771) Add safety check on bank module perms to allow module-specific mint restrictions (e.g. only minting a certain denom).
* (x/bank) [\#10771](https://github.com/cosmos/cosmos-sdk/pull/10771) Add `bank.BankKeeper.WithMintCoinsRestriction` function to restrict use of bank `MintCoins` usage. This function is not on the bank `Keeper` interface, so it's not API-breaking, but only additive on the keeper implementation.
* [\#11124](https://github.com/cosmos/cosmos-sdk/pull/11124) Add `GetAllVersions` to application store
Expand Down
30 changes: 30 additions & 0 deletions proto/cosmos/bank/v1beta1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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 @@ -88,6 +94,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;

// 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
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,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) {
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())
}

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)

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

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
58 changes: 55 additions & 3 deletions x/bank/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ package keeper_test
import (
gocontext "context"
"fmt"
"time"

"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/simapp"

minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"

"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/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
)

func (suite *IntegrationTestSuite) TestQueryBalance() {
Expand Down Expand Up @@ -86,6 +88,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(simapp.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
expectedTotalSupply := sdk.NewCoins(sdk.NewInt64Coin("test", 400000000))
Expand Down
10 changes: 4 additions & 6 deletions x/bank/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ import (
"testing"
"time"

"github.com/cosmos/cosmos-sdk/types/query"

"github.com/cosmos/cosmos-sdk/x/auth/vesting/exported"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"

"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
Expand All @@ -18,11 +13,14 @@ import (
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting/exported"
vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
"github.com/cosmos/cosmos-sdk/x/bank/keeper"
"github.com/cosmos/cosmos-sdk/x/bank/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
)

const (
Expand Down Expand Up @@ -97,7 +95,7 @@ func (suite *IntegrationTestSuite) initKeepersWithmAccPerms(blockedAddrs map[str

func (suite *IntegrationTestSuite) SetupTest() {
app := simapp.Setup(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
Original file line number Diff line number Diff line change
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