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 JeancarloBarrios committed Sep 28, 2024
1 parent c00dbd3 commit 3e7e7f4
Show file tree
Hide file tree
Showing 7 changed files with 722 additions and 359 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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
36 changes: 36 additions & 0 deletions x/bank/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,42 @@ func (k BaseKeeper) SpendableBalanceByDenom(ctx context.Context, req *types.Quer
return &types.QuerySpendableBalanceByDenomResponse{Balance: &spendable}, 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) {
totalSupply, pageRes, err := k.GetPaginatedTotalSupply(ctx, req.Pagination)
Expand Down
75 changes: 45 additions & 30 deletions x/bank/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import (
"fmt"
"time"

v1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1"
"cosmossdk.io/core/header"
"cosmossdk.io/x/bank/testutil"
"cosmossdk.io/x/bank/types"

"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/simapp"
"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 *KeeperTestSuite) TestQueryBalance() {
Expand Down Expand Up @@ -231,46 +230,62 @@ func (suite *KeeperTestSuite) TestSpendableBalances() {
suite.EqualValues(25, res.Balances[1].Amount.Int64())
}

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

ctx := sdk.UnwrapSDKContext(suite.ctx)
ctx = ctx.WithHeaderInfo(header.Info{Time: time.Now()})
queryClient := suite.mockQueryClient(ctx)

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

addrStr, err := suite.authKeeper.AddressCodec().BytesToString(addr)
suite.Require().NoError(err)

req := types.NewQuerySpendableBalanceByDenomRequest(addrStr, fooDenom)
acc := authtypes.NewBaseAccountWithAddress(addr)
pageReq := &query.PageRequest{
Key: nil,
Limit: 2,
CountTotal: false,
}
req := types.NewQuerySpendableBalancesRequest(addr, pageReq)

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

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

origCoins := sdk.NewCoins(fooCoins, barCoins)
vacc, err := vestingtypes.NewContinuousVestingAccount(
acc,
acc := app.AccountKeeper.NewAccountWithAddress(ctx, addr)
acc = vestingtypes.NewContinuousVestingAccount(
acc.(*authtypes.BaseAccount),
sdk.NewCoins(fooCoins),
ctx.HeaderInfo().Time.Unix(),
ctx.HeaderInfo().Time.Add(time.Hour).Unix(),
ctx.BlockTime().Unix(),
ctx.BlockTime().Add(time.Hour).Unix(),
)
suite.Require().NoError(err)

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

// move time forward for half of the tokens to vest
ctx = ctx.WithHeaderInfo(header.Info{Time: ctx.HeaderInfo().Time.Add(30 * time.Minute)})
queryClient = suite.mockQueryClient(ctx)
// 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))
suite.
Require().
NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, expectedTotalSupply))

// check fooCoins first, it has some vested and some vesting
suite.mockSpendableCoins(ctx, vacc)
Expand Down
16 changes: 8 additions & 8 deletions x/bank/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"testing"
"time"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/suite"

coreevent "cosmossdk.io/core/event"
Expand All @@ -24,11 +23,14 @@ import (
"github.com/cosmos/cosmos-sdk/runtime"
"github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
"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 @@ -125,11 +127,9 @@ func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

func (suite *KeeperTestSuite) SetupTest() {
key := storetypes.NewKVStoreKey(banktypes.StoreKey)
testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test"))
ctx := testCtx.Ctx.WithHeaderInfo(header.Info{Time: time.Now()})
encCfg := moduletestutil.MakeTestEncodingConfig(codectestutil.CodecOptions{})
func (suite *IntegrationTestSuite) SetupTest() {
app := simapp.Setup(false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Now()})

env := runtime.NewEnvironment(runtime.NewKVStoreService(key), coretesting.NewNopLogger())

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 @@ -34,6 +34,13 @@ func NewQuerySpendableBalanceByDenomRequest(addr, denom string) *QuerySpendableB
return &QuerySpendableBalanceByDenomRequest{Address: addr, Denom: denom}
}

// 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'
type QueryTotalSupplyParams struct {
Expand Down
Loading

0 comments on commit 3e7e7f4

Please sign in to comment.