Skip to content

Commit

Permalink
feat: add support for spendable balances gRPC query (backport cosmos#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored and randy75828 committed Aug 10, 2022
1 parent 068557e commit 04af0ef
Show file tree
Hide file tree
Showing 8 changed files with 821 additions and 72 deletions.
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.
* (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
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
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
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
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
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

0 comments on commit 04af0ef

Please sign in to comment.