From ef9968debd1fa4c492d7e1df6c8cf146c1ede6d0 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja <33157909+fdymylja@users.noreply.github.com> Date: Thu, 25 Feb 2021 19:15:02 +0100 Subject: [PATCH] [bank]: add balance tracking events (#8656) * change(bank): add utxo events and simplify logic * add(bank): balance and supply tracking test * chore(bank): fix balance tracking test comment * fix(grpc): service test * fix(bank): sub unlocked coins to use less gas * fix(auth): cli test gas * fix(rest): grpc gas test * fix(staking/cli): increase delegation required gas * add: burn events, fix tests * fix(auth/tx): grpc test * add(bank): coin events in delegate * fix(bank): add amt check in delegate coins back * change(bank): add coin spent and coin recv events in burn and mint * change(bank): revert sub coin function * change(auth): revert cli test * change(auth): revert service test * chore(auth): fix events comment in service_test.go * chore: update CHANGELOG.md * remove(bank): balanceError func * chore(bank): address lint warnings * chore(bank): update events spec Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + x/auth/client/cli/cli_test.go | 9 +- x/auth/tx/service_test.go | 9 +- x/bank/keeper/genesis_test.go | 4 +- x/bank/keeper/keeper.go | 26 +++-- x/bank/keeper/keeper_test.go | 129 +++++++++++++++++++--- x/bank/keeper/send.go | 29 +++-- x/bank/keeper/view.go | 16 ++- x/bank/spec/04_events.md | 120 ++++++++++++++++++++ x/bank/types/events.go | 55 +++++++++ x/staking/client/cli/cli_test.go | 10 +- x/staking/client/rest/grpc_query_test.go | 14 ++- x/staking/client/testutil/test_helpers.go | 2 + 13 files changed, 375 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5cc29513f54..feae9a1997ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (x/ibc) [\#8405](https://github.com/cosmos/cosmos-sdk/pull/8405) Refactor IBC client update governance proposals to use a substitute client to update a frozen or expired client. * (x/evidence) [\#8502](https://github.com/cosmos/cosmos-sdk/pull/8502) `HandleEquivocationEvidence` persists the evidence to state. * (x/gov) [\#7733](https://github.com/cosmos/cosmos-sdk/pull/7733) ADR 037 Implementation: Governance Split Votes +* (x/bank) [\#8656](https://github.com/cosmos/cosmos-sdk/pull/8656) balance and supply are now correctly tracked via `coin_spent`, `coin_received`, `coinbase` and `burn` events. ### Improvements diff --git a/x/auth/client/cli/cli_test.go b/x/auth/client/cli/cli_test.go index 6cd102cade97..6d9d776f1b03 100644 --- a/x/auth/client/cli/cli_test.go +++ b/x/auth/client/cli/cli_test.go @@ -1102,7 +1102,7 @@ func (s *IntegrationTestSuite) TestSignWithMultiSigners_AminoJSON() { banktypes.NewMsgSend(val1.Address, addr1, sdk.NewCoins(val1Coin)), ) txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)))) - txBuilder.SetGasLimit(testdata.NewTestGasLimit()) + txBuilder.SetGasLimit(testdata.NewTestGasLimit()) // min required is 101892 require.Equal([]sdk.AccAddress{val0.Address, val1.Address}, txBuilder.GetTx().GetSigners()) // Write the unsigned tx into a file. @@ -1126,7 +1126,12 @@ func (s *IntegrationTestSuite) TestSignWithMultiSigners_AminoJSON() { signedTxFile := testutil.WriteToNewTempFile(s.T(), signedTx.String()) // Now let's try to send this tx. - res, err := authtest.TxBroadcastExec(val0.ClientCtx, signedTxFile.Name(), fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock)) + res, err := authtest.TxBroadcastExec( + val0.ClientCtx, + signedTxFile.Name(), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + ) + require.NoError(err) var txRes sdk.TxResponse require.NoError(val0.ClientCtx.JSONMarshaler.UnmarshalJSON(res.Bytes(), &txRes)) diff --git a/x/auth/tx/service_test.go b/x/auth/tx/service_test.go index 4f5db8e9c99d..58b21c139772 100644 --- a/x/auth/tx/service_test.go +++ b/x/auth/tx/service_test.go @@ -5,13 +5,14 @@ import ( "fmt" "testing" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + "github.com/stretchr/testify/suite" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" clienttx "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/testutil/network" - "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" query "github.com/cosmos/cosmos-sdk/types/query" @@ -106,7 +107,7 @@ func (s IntegrationTestSuite) TestSimulateTx_GRPC() { } else { s.Require().NoError(err) // Check the result and gas used are correct. - s.Require().Equal(len(res.GetResult().GetEvents()), 4) // 1 transfer, 3 messages. + s.Require().Equal(len(res.GetResult().GetEvents()), 6) // 1 coin recv 1 coin spent, 1 transfer, 3 messages. s.Require().True(res.GetGasInfo().GetGasUsed() > 0) // Gas used sometimes change, just check it's not empty. } }) @@ -143,7 +144,7 @@ func (s IntegrationTestSuite) TestSimulateTx_GRPCGateway() { err = val.ClientCtx.JSONMarshaler.UnmarshalJSON(res, &result) s.Require().NoError(err) // Check the result and gas used are correct. - s.Require().Equal(len(result.GetResult().GetEvents()), 4) // 1 transfer, 3 messages. + s.Require().Equal(len(result.GetResult().GetEvents()), 6) // 1 coin recv, 1 coin spent,1 transfer, 3 messages. s.Require().True(result.GetGasInfo().GetGasUsed() > 0) // Gas used sometimes change, just check it's not empty. } }) @@ -412,7 +413,7 @@ func (s IntegrationTestSuite) TestBroadcastTx_GRPCGateway() { var result tx.BroadcastTxResponse err = val.ClientCtx.JSONMarshaler.UnmarshalJSON(res, &result) s.Require().NoError(err) - s.Require().Equal(uint32(0), result.TxResponse.Code) + s.Require().Equal(uint32(0), result.TxResponse.Code, "rawlog", result.TxResponse.RawLog) } }) } diff --git a/x/bank/keeper/genesis_test.go b/x/bank/keeper/genesis_test.go index 7bf7dc920315..35e56c1295d1 100644 --- a/x/bank/keeper/genesis_test.go +++ b/x/bank/keeper/genesis_test.go @@ -25,8 +25,6 @@ func (suite *IntegrationTestSuite) TestExportGenesis() { Require(). NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, accAddr, expectedBalances[i].Coins)) } - // add mint module balance as nil - expectedBalances = append(expectedBalances, types.Balance{Address: "cosmos1m3h30wlvsf8llruxtpukdvsy0km2kum8g38c8q", Coins: nil}) app.BankKeeper.SetParams(ctx, types.DefaultParams()) exportGenesis := app.BankKeeper.ExportGenesis(ctx) @@ -34,6 +32,8 @@ func (suite *IntegrationTestSuite) TestExportGenesis() { suite.Require().Len(exportGenesis.Params.SendEnabled, 0) suite.Require().Equal(types.DefaultParams().DefaultSendEnabled, exportGenesis.Params.DefaultSendEnabled) suite.Require().Equal(totalSupply.Total, exportGenesis.Supply) + // add mint module balance as nil + expectedBalances = append(expectedBalances, types.Balance{Address: "cosmos1m3h30wlvsf8llruxtpukdvsy0km2kum8g38c8q", Coins: nil}) suite.Require().Equal(expectedBalances, exportGenesis.Balances) suite.Require().Equal(expectedMetadata, exportGenesis.DenomMetadata) } diff --git a/x/bank/keeper/keeper.go b/x/bank/keeper/keeper.go index 153d5d4a55d9..c65a3ab7c0f0 100644 --- a/x/bank/keeper/keeper.go +++ b/x/bank/keeper/keeper.go @@ -1,8 +1,6 @@ package keeper import ( - "time" - "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" @@ -107,9 +105,13 @@ func (k BaseKeeper) DelegateCoins(ctx sdk.Context, delegatorAddr, moduleAccAddr } } - if err := k.trackDelegation(ctx, delegatorAddr, ctx.BlockHeader().Time, balances, amt); err != nil { + if err := k.trackDelegation(ctx, delegatorAddr, balances, amt); err != nil { return sdkerrors.Wrap(err, "failed to track delegation") } + // emit coin spent event + ctx.EventManager().EmitEvent( + types.NewCoinSpentEvent(delegatorAddr, amt), + ) err := k.addCoins(ctx, moduleAccAddr, amt) if err != nil { @@ -134,7 +136,7 @@ func (k BaseKeeper) UndelegateCoins(ctx sdk.Context, moduleAccAddr, delegatorAdd return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } - err := k.subtractCoins(ctx, moduleAccAddr, amt) + err := k.subUnlockedCoins(ctx, moduleAccAddr, amt) if err != nil { return err } @@ -345,6 +347,11 @@ func (k BaseKeeper) MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) logger := k.Logger(ctx) logger.Info("minted coins from module account", "amount", amt.String(), "from", moduleName) + // emit mint event + ctx.EventManager().EmitEvent( + types.NewCoinMintEvent(acc.GetAddress(), amt), + ) + return nil } @@ -360,7 +367,7 @@ func (k BaseKeeper) BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) panic(sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "module account %s does not have permissions to burn tokens", moduleName)) } - err := k.subtractCoins(ctx, acc.GetAddress(), amt) + err := k.subUnlockedCoins(ctx, acc.GetAddress(), amt) if err != nil { return err } @@ -373,10 +380,15 @@ func (k BaseKeeper) BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) logger := k.Logger(ctx) logger.Info("burned tokens from module account", "amount", amt.String(), "from", moduleName) + // emit burn event + ctx.EventManager().EmitEvent( + types.NewCoinBurnEvent(acc.GetAddress(), amt), + ) + return nil } -func (k BaseKeeper) trackDelegation(ctx sdk.Context, addr sdk.AccAddress, blockTime time.Time, balance, amt sdk.Coins) error { +func (k BaseKeeper) trackDelegation(ctx sdk.Context, addr sdk.AccAddress, balance, amt sdk.Coins) error { acc := k.ak.GetAccount(ctx, addr) if acc == nil { return sdkerrors.Wrapf(sdkerrors.ErrUnknownAddress, "account %s does not exist", addr) @@ -385,7 +397,7 @@ func (k BaseKeeper) trackDelegation(ctx sdk.Context, addr sdk.AccAddress, blockT vacc, ok := acc.(vestexported.VestingAccount) if ok { // TODO: return error on account.TrackDelegation - vacc.TrackDelegation(blockTime, balance, amt) + vacc.TrackDelegation(ctx.BlockHeader().Time, balance, amt) } return nil diff --git a/x/bank/keeper/keeper_test.go b/x/bank/keeper/keeper_test.go index 97d0b3a8769b..ffd4838d036b 100644 --- a/x/bank/keeper/keeper_test.go +++ b/x/bank/keeper/keeper_test.go @@ -153,17 +153,17 @@ func (suite *IntegrationTestSuite) TestSupply_SendCoins() { suite.Require().NoError( keeper.SendCoinsFromModuleToModule(ctx, holderAcc.GetName(), authtypes.Burner, initCoins), ) - suite.Require().Equal(sdk.Coins(nil), getCoinsByName(ctx, keeper, authKeeper, holderAcc.GetName())) + suite.Require().Equal(sdk.NewCoins().String(), getCoinsByName(ctx, keeper, authKeeper, holderAcc.GetName()).String()) suite.Require().Equal(initCoins, getCoinsByName(ctx, keeper, authKeeper, authtypes.Burner)) suite.Require().NoError( keeper.SendCoinsFromModuleToAccount(ctx, authtypes.Burner, baseAcc.GetAddress(), initCoins), ) - suite.Require().Equal(sdk.Coins(nil), getCoinsByName(ctx, keeper, authKeeper, authtypes.Burner)) + suite.Require().Equal(sdk.NewCoins().String(), getCoinsByName(ctx, keeper, authKeeper, authtypes.Burner).String()) suite.Require().Equal(initCoins, keeper.GetAllBalances(ctx, baseAcc.GetAddress())) suite.Require().NoError(keeper.SendCoinsFromAccountToModule(ctx, baseAcc.GetAddress(), authtypes.Burner, initCoins)) - suite.Require().Equal(sdk.Coins(nil), keeper.GetAllBalances(ctx, baseAcc.GetAddress())) + suite.Require().Equal(sdk.NewCoins().String(), keeper.GetAllBalances(ctx, baseAcc.GetAddress()).String()) suite.Require().Equal(initCoins, getCoinsByName(ctx, keeper, authKeeper, authtypes.Burner)) } @@ -266,7 +266,7 @@ func (suite *IntegrationTestSuite) TestSupply_BurnCoins() { err = keeper.BurnCoins(ctx, authtypes.Burner, initCoins) suite.Require().NoError(err) - suite.Require().Equal(sdk.Coins(nil), getCoinsByName(ctx, keeper, authKeeper, authtypes.Burner)) + suite.Require().Equal(sdk.NewCoins().String(), getCoinsByName(ctx, keeper, authKeeper, authtypes.Burner).String()) suite.Require().Equal(supplyAfterInflation.GetTotal().Sub(initCoins), keeper.GetSupply(ctx).GetTotal()) // test same functionality on module account with multiple permissions @@ -280,7 +280,7 @@ func (suite *IntegrationTestSuite) TestSupply_BurnCoins() { err = keeper.BurnCoins(ctx, multiPermAcc.GetName(), initCoins) suite.Require().NoError(err) - suite.Require().Equal(sdk.Coins(nil), getCoinsByName(ctx, keeper, authKeeper, multiPermAcc.GetName())) + suite.Require().Equal(sdk.NewCoins().String(), getCoinsByName(ctx, keeper, authKeeper, multiPermAcc.GetName()).String()) suite.Require().Equal(supplyAfterInflation.GetTotal().Sub(initCoins), keeper.GetSupply(ctx).GetTotal()) } @@ -537,6 +537,7 @@ func (suite *IntegrationTestSuite) TestMsgSendEvents() { event1.Attributes, abci.EventAttribute{Key: []byte(sdk.AttributeKeyAmount), Value: []byte(newCoins.String())}, ) + event2 := sdk.Event{ Type: sdk.EventTypeMessage, Attributes: []abci.EventAttribute{}, @@ -556,9 +557,9 @@ func (suite *IntegrationTestSuite) TestMsgSendEvents() { // events are shifted due to the funding account events events = ctx.EventManager().ABCIEvents() - suite.Require().Equal(6, len(events)) - suite.Require().Equal(abci.Event(event1), events[4]) - suite.Require().Equal(abci.Event(event2), events[5]) + suite.Require().Equal(12, len(events)) + suite.Require().Equal(abci.Event(event1), events[8]) + suite.Require().Equal(abci.Event(event2), events[9]) } func (suite *IntegrationTestSuite) TestMsgMultiSendEvents() { @@ -597,7 +598,7 @@ func (suite *IntegrationTestSuite) TestMsgMultiSendEvents() { suite.Require().Error(app.BankKeeper.InputOutputCoins(ctx, inputs, outputs)) events = ctx.EventManager().ABCIEvents() - suite.Require().Equal(3, len(events)) // 3 events because minting event is there + suite.Require().Equal(8, len(events)) // 7 events because account funding causes extra minting + coin_spent + coin_recv events event1 := sdk.Event{ Type: sdk.EventTypeMessage, @@ -607,7 +608,7 @@ func (suite *IntegrationTestSuite) TestMsgMultiSendEvents() { event1.Attributes, abci.EventAttribute{Key: []byte(types.AttributeKeySender), Value: []byte(addr.String())}, ) - suite.Require().Equal(abci.Event(event1), events[2]) // it's the third event since we have the minting event before + suite.Require().Equal(abci.Event(event1), events[7]) // Set addr's coins and addr2's coins suite.Require().NoError(simapp.FundAccount(app, ctx, addr, sdk.NewCoins(sdk.NewInt64Coin(fooDenom, 50)))) @@ -619,7 +620,7 @@ func (suite *IntegrationTestSuite) TestMsgMultiSendEvents() { suite.Require().NoError(app.BankKeeper.InputOutputCoins(ctx, inputs, outputs)) events = ctx.EventManager().ABCIEvents() - suite.Require().Equal(11, len(events)) + suite.Require().Equal(28, len(events)) // 25 due to account funding + coin_spent + coin_recv events event2 := sdk.Event{ Type: sdk.EventTypeMessage, @@ -652,12 +653,11 @@ func (suite *IntegrationTestSuite) TestMsgMultiSendEvents() { event4.Attributes, abci.EventAttribute{Key: []byte(sdk.AttributeKeyAmount), Value: []byte(newCoins2.String())}, ) - // events are shifted due to the funding account events - suite.Require().Equal(abci.Event(event1), events[7]) - suite.Require().Equal(abci.Event(event2), events[8]) - suite.Require().Equal(abci.Event(event3), events[9]) - suite.Require().Equal(abci.Event(event4), events[10]) + suite.Require().Equal(abci.Event(event1), events[21]) + suite.Require().Equal(abci.Event(event2), events[23]) + suite.Require().Equal(abci.Event(event3), events[25]) + suite.Require().Equal(abci.Event(event4), events[27]) } func (suite *IntegrationTestSuite) TestSpendableCoins() { @@ -1002,6 +1002,103 @@ func (suite *IntegrationTestSuite) TestIterateAllDenomMetaData() { } } +func (suite *IntegrationTestSuite) TestBalanceTrackingEvents() { + // replace account keeper and bank keeper otherwise the account keeper won't be aware of the + // existence of the new module account because GetModuleAccount checks for the existence via + // permissions map and not via state... weird + maccPerms := simapp.GetMaccPerms() + maccPerms[multiPerm] = []string{authtypes.Burner, authtypes.Minter, authtypes.Staking} + + suite.app.AccountKeeper = authkeeper.NewAccountKeeper( + suite.app.AppCodec(), suite.app.GetKey(authtypes.StoreKey), suite.app.GetSubspace(authtypes.ModuleName), + authtypes.ProtoBaseAccount, maccPerms, + ) + + suite.app.BankKeeper = keeper.NewBaseKeeper(suite.app.AppCodec(), suite.app.GetKey(types.StoreKey), + suite.app.AccountKeeper, suite.app.GetSubspace(types.ModuleName), nil) + + // set account with multiple permissions + suite.app.AccountKeeper.SetModuleAccount(suite.ctx, multiPermAcc) + // mint coins + suite.Require().NoError( + suite.app.BankKeeper.MintCoins( + suite.ctx, + multiPermAcc.Name, + sdk.NewCoins(sdk.NewCoin("utxo", sdk.NewInt(100000)))), + ) + // send coins to address + addr1 := sdk.AccAddress("addr1_______________") + suite.Require().NoError( + suite.app.BankKeeper.SendCoinsFromModuleToAccount( + suite.ctx, + multiPermAcc.Name, + addr1, + sdk.NewCoins(sdk.NewCoin("utxo", sdk.NewInt(50000))), + ), + ) + + // burn coins from module account + suite.Require().NoError( + suite.app.BankKeeper.BurnCoins( + suite.ctx, + multiPermAcc.Name, + sdk.NewCoins(sdk.NewInt64Coin("utxo", 1000)), + ), + ) + + // process balances and supply from events + supply := sdk.NewCoins() + + balances := make(map[string]sdk.Coins) + + for _, e := range suite.ctx.EventManager().ABCIEvents() { + switch e.Type { + case types.EventTypeCoinBurn: + burnedCoins, err := sdk.ParseCoinsNormalized((string)(e.Attributes[1].Value)) + suite.Require().NoError(err) + supply = supply.Sub(burnedCoins) + + case types.EventTypeCoinMint: + mintedCoins, err := sdk.ParseCoinsNormalized((string)(e.Attributes[1].Value)) + suite.Require().NoError(err) + supply = supply.Add(mintedCoins...) + + case types.EventTypeCoinSpent: + coinsSpent, err := sdk.ParseCoinsNormalized((string)(e.Attributes[1].Value)) + suite.Require().NoError(err) + spender, err := sdk.AccAddressFromBech32((string)(e.Attributes[0].Value)) + suite.Require().NoError(err) + balances[spender.String()] = balances[spender.String()].Sub(coinsSpent) + + case types.EventTypeCoinReceived: + coinsRecv, err := sdk.ParseCoinsNormalized((string)(e.Attributes[1].Value)) + suite.Require().NoError(err) + receiver, err := sdk.AccAddressFromBech32((string)(e.Attributes[0].Value)) + suite.Require().NoError(err) + balances[receiver.String()] = balances[receiver.String()].Add(coinsRecv...) + } + } + + // check balance and supply tracking + savedSupply := suite.app.BankKeeper.GetSupply(suite.ctx) + utxoSupply := savedSupply.GetTotal().AmountOf("utxo") + suite.Require().Equal(utxoSupply, supply.AmountOf("utxo")) + // iterate accounts and check balances + suite.app.BankKeeper.IterateAllBalances(suite.ctx, func(address sdk.AccAddress, coin sdk.Coin) (stop bool) { + // if it's not utxo coin then skip + if coin.Denom != "utxo" { + return false + } + + balance, exists := balances[address.String()] + suite.Require().True(exists) + + expectedUtxo := sdk.NewCoin("utxo", balance.AmountOf(coin.Denom)) + suite.Require().Equal(expectedUtxo.String(), coin.String()) + return false + }) +} + func (suite *IntegrationTestSuite) getTestMetadata() []types.Metadata { return []types.Metadata{{ Name: "Cosmos Hub Atom", diff --git a/x/bank/keeper/send.go b/x/bank/keeper/send.go index c0c3a4cb62dc..0093a3b805a5 100644 --- a/x/bank/keeper/send.go +++ b/x/bank/keeper/send.go @@ -83,7 +83,7 @@ func (k BaseSendKeeper) InputOutputCoins(ctx sdk.Context, inputs []types.Input, return err } - err = k.subtractCoins(ctx, inAddress, in.Coins) + err = k.subUnlockedCoins(ctx, inAddress, in.Coins) if err != nil { return err } @@ -144,7 +144,7 @@ func (k BaseSendKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAd ), }) - err := k.subtractCoins(ctx, fromAddr, amt) + err := k.subUnlockedCoins(ctx, fromAddr, amt) if err != nil { return err } @@ -167,9 +167,10 @@ func (k BaseSendKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAd return nil } -// subtractCoins removes amt coins the account by the given address. An error is +// subUnlockedCoins removes the unlocked amt coins of the given account. An error is // returned if the resulting balance is negative or the initial amount is invalid. -func (k BaseSendKeeper) subtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) error { +// A coin_spent event is emitted after. +func (k BaseSendKeeper) subUnlockedCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) error { if !amt.IsValid() { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } @@ -194,12 +195,15 @@ func (k BaseSendKeeper) subtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt } } + // emit coin spent event + ctx.EventManager().EmitEvent( + types.NewCoinSpentEvent(addr, amt), + ) return nil } -// addCoins adds amt to the account balance given by the provided address. An -// error is returned if the initial amount is invalid or if any resulting new -// balance is negative. +// addCoins increase the addr balance by the given amt. Fails if the provided amt is invalid. +// It emits a coin received event. func (k BaseSendKeeper) addCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) error { if !amt.IsValid() { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) @@ -215,11 +219,16 @@ func (k BaseSendKeeper) addCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.C } } + // emit coin received event + ctx.EventManager().EmitEvent( + types.NewCoinReceivedEvent(addr, amt), + ) + return nil } -// ClearBalances removes all balances for a given account by address. -func (k BaseSendKeeper) ClearBalances(ctx sdk.Context, addr sdk.AccAddress) { +// clearBalances removes all balances for a given account by address. +func (k BaseSendKeeper) clearBalances(ctx sdk.Context, addr sdk.AccAddress) { keys := [][]byte{} k.IterateAccountBalances(ctx, addr, func(balance sdk.Coin) bool { keys = append(keys, []byte(balance.Denom)) @@ -237,7 +246,7 @@ func (k BaseSendKeeper) ClearBalances(ctx sdk.Context, addr sdk.AccAddress) { // clear out all balances prior to setting the new coins as to set existing balances // to zero if they don't exist in amt. An error is returned upon failure. func (k BaseSendKeeper) setBalances(ctx sdk.Context, addr sdk.AccAddress, balances sdk.Coins) error { - k.ClearBalances(ctx, addr) + k.clearBalances(ctx, addr) for _, balance := range balances { err := k.setBalance(ctx, addr, balance) diff --git a/x/bank/keeper/view.go b/x/bank/keeper/view.go index 1b50fb81d3aa..7271bba05945 100644 --- a/x/bank/keeper/view.go +++ b/x/bank/keeper/view.go @@ -171,15 +171,23 @@ func (k BaseViewKeeper) LockedCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Co // by address. If the account has no spendable coins, an empty Coins slice is // returned. func (k BaseViewKeeper) SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins { - balances := k.GetAllBalances(ctx, addr) + spendable, _ := k.spendableCoins(ctx, addr) + return spendable +} + +// spendableCoins returns the coins the given address can spend alongside the total amount of coins it holds. +// It exists for gas efficiency, in order to avoid to have to get balance multiple times. +func (k BaseViewKeeper) spendableCoins(ctx sdk.Context, addr sdk.AccAddress) (spendable, total sdk.Coins) { + total = k.GetAllBalances(ctx, addr) locked := k.LockedCoins(ctx, addr) - spendable, hasNeg := balances.SafeSub(locked) + spendable, hasNeg := total.SafeSub(locked) if hasNeg { - return sdk.NewCoins() + spendable = sdk.NewCoins() + return } - return spendable + return } // ValidateBalance validates all balances for a given account address returning diff --git a/x/bank/spec/04_events.md b/x/bank/spec/04_events.md index 55c93b805ed1..71c068b0835a 100644 --- a/x/bank/spec/04_events.md +++ b/x/bank/spec/04_events.md @@ -27,3 +27,123 @@ The bank module emits the following events: | message | module | bank | | message | action | multisend | | message | sender | {senderAddress} | + +## Keeper events + +In addition to handlers events, the bank keeper will produce events when the following methods are called (or any method which ends up calling them) + +### MintCoins + +```json +{ + "type": "coinbase", + "attributes": [ + { + "key": "minter", + "value": "{{sdk.AccAddress of the module minting coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being minted}}", + "index": true + } + ] +} +``` + +```json +{ + "type": "coin_received", + "attributes": [ + { + "key": "receiver", + "value": "{{sdk.AccAddress of the module minting coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being received}}", + "index": true + } + ] +} +``` + +### BurnCoins + +```json +{ + "type": "burn", + "attributes": [ + { + "key": "burner", + "value": "{{sdk.AccAddress of the module burning coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being burned}}", + "index": true + } + ] +} +``` + +```json +{ + "type": "coin_spent", + "attributes": [ + { + "key": "spender", + "value": "{{sdk.AccAddress of the module burning coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being burned}}", + "index": true + } + ] +} +``` + +### addCoins + +```json +{ + "type": "coin_received", + "attributes": [ + { + "key": "receiver", + "value": "{{sdk.AccAddress of the address beneficiary of the coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being received}}", + "index": true + } + ] +} +``` + +### subUnlockedCoins/DelegateCoins + +```json +{ + "type": "coin_spent", + "attributes": [ + { + "key": "spender", + "value": "{{sdk.AccAddress of the address which is spending coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being spent}}", + "index": true + } + ] +} +``` \ No newline at end of file diff --git a/x/bank/types/events.go b/x/bank/types/events.go index c03d142f00b6..9f06b85e491c 100644 --- a/x/bank/types/events.go +++ b/x/bank/types/events.go @@ -1,5 +1,9 @@ package types +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + // bank module event types const ( EventTypeTransfer = "transfer" @@ -8,4 +12,55 @@ const ( AttributeKeySender = "sender" AttributeValueCategory = ModuleName + + // supply and balance tracking events name and attributes + EventTypeCoinSpent = "coin_spent" + EventTypeCoinReceived = "coin_received" + EventTypeCoinMint = "coinbase" // NOTE(fdymylja): using mint clashes with mint module event + EventTypeCoinBurn = "burn" + + AttributeKeySpender = "spender" + AttributeKeyReceiver = "receiver" + AttributeKeyMinter = "minter" + AttributeKeyBurner = "burner" ) + +// NewCoinSpentEvent constructs a new coin spent sdk.Event +// nolint: interfacer +func NewCoinSpentEvent(spender sdk.AccAddress, amount sdk.Coins) sdk.Event { + return sdk.NewEvent( + EventTypeCoinSpent, + sdk.NewAttribute(AttributeKeySpender, spender.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()), + ) +} + +// NewCoinReceivedEvent constructs a new coin received sdk.Event +// nolint: interfacer +func NewCoinReceivedEvent(receiver sdk.AccAddress, amount sdk.Coins) sdk.Event { + return sdk.NewEvent( + EventTypeCoinReceived, + sdk.NewAttribute(AttributeKeyReceiver, receiver.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()), + ) +} + +// NewCoinMintEvent construct a new coin minted sdk.Event +// nolint: interfacer +func NewCoinMintEvent(minter sdk.AccAddress, amount sdk.Coins) sdk.Event { + return sdk.NewEvent( + EventTypeCoinMint, + sdk.NewAttribute(AttributeKeyMinter, minter.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()), + ) +} + +// NewCoinBurnEvent constructs a new coin burned sdk.Event +// nolint: interfacer +func NewCoinBurnEvent(burner sdk.AccAddress, amount sdk.Coins) sdk.Event { + return sdk.NewEvent( + EventTypeCoinBurn, + sdk.NewAttribute(AttributeKeyBurner, burner.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()), + ) +} diff --git a/x/staking/client/cli/cli_test.go b/x/staking/client/cli/cli_test.go index 73f60e22c048..adaa825f92b7 100644 --- a/x/staking/client/cli/cli_test.go +++ b/x/staking/client/cli/cli_test.go @@ -58,11 +58,17 @@ func (s *IntegrationTestSuite) SetupSuite() { val2 := s.network.Validators[1] // redelegate - _, err = stakingtestutil.MsgRedelegateExec(val.ClientCtx, val.Address, val.ValAddress, val2.ValAddress, unbond) + _, err = stakingtestutil.MsgRedelegateExec( + val.ClientCtx, + val.Address, + val.ValAddress, + val2.ValAddress, + unbond, + fmt.Sprintf("--%s=%d", flags.FlagGas, 202954), // 202954 is the required + ) s.Require().NoError(err) _, err = s.network.WaitForHeight(1) s.Require().NoError(err) - // unbonding _, err = stakingtestutil.MsgUnbondExec(val.ClientCtx, val.Address, val.ValAddress, unbond) s.Require().NoError(err) diff --git a/x/staking/client/rest/grpc_query_test.go b/x/staking/client/rest/grpc_query_test.go index 696a55ff9bd2..69e9f34adc23 100644 --- a/x/staking/client/rest/grpc_query_test.go +++ b/x/staking/client/rest/grpc_query_test.go @@ -6,6 +6,8 @@ import ( "fmt" "testing" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/gogo/protobuf/proto" "github.com/stretchr/testify/suite" @@ -46,7 +48,15 @@ func (s *IntegrationTestSuite) SetupSuite() { val2 := s.network.Validators[1] // redelegate - _, err = stakingtestutil.MsgRedelegateExec(val.ClientCtx, val.Address, val.ValAddress, val2.ValAddress, unbond) + _, err = stakingtestutil.MsgRedelegateExec( + val.ClientCtx, + val.Address, + val.ValAddress, + val2.ValAddress, + unbond, + fmt.Sprintf("--%s=%d", flags.FlagGas, 254000), + ) // expected gas is 202987 + s.Require().NoError(err) _, err = s.network.WaitForHeight(1) s.Require().NoError(err) @@ -330,7 +340,7 @@ func (s *IntegrationTestSuite) TestQueryDelegationGRPC() { s.Run(tc.name, func() { resp, err := rest.GetRequest(tc.url) s.Require().NoError(err) - + s.T().Logf("%s", resp) err = val.ClientCtx.JSONMarshaler.UnmarshalJSON(resp, tc.respType) if tc.error { diff --git a/x/staking/client/testutil/test_helpers.go b/x/staking/client/testutil/test_helpers.go index 1a2a4b84bcbf..ad60b96a4f8a 100644 --- a/x/staking/client/testutil/test_helpers.go +++ b/x/staking/client/testutil/test_helpers.go @@ -27,6 +27,7 @@ func MsgRedelegateExec(clientCtx client.Context, from, src, dst, amount fmt.Stri amount.String(), fmt.Sprintf("--%s=%s", flags.FlagFrom, from.String()), } + args = append(args, extraArgs...) args = append(args, commonArgs...) return clitestutil.ExecTestCLICmd(clientCtx, stakingcli.NewRedelegateCmd(), args) @@ -43,5 +44,6 @@ func MsgUnbondExec(clientCtx client.Context, from fmt.Stringer, valAddress, } args = append(args, commonArgs...) + args = append(args, extraArgs...) return clitestutil.ExecTestCLICmd(clientCtx, stakingcli.NewUnbondCmd(), args) }