diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1d49eb5ea..958101aba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ # CODEOWNERS: https://help.github.com/articles/about-codeowners/ # Primary repo maintainers -* @joe-bowman @ajansari95 @muku314115 @ThanhNhann @faddat @sontrinh16 @anhductn2001 +* @joe-bowman @ThanhNhann @faddat @sontrinh16 @anhductn2001 diff --git a/x/interchainstaking/keeper/callbacks.go b/x/interchainstaking/keeper/callbacks.go index 78d3b8a12..622b50b43 100644 --- a/x/interchainstaking/keeper/callbacks.go +++ b/x/interchainstaking/keeper/callbacks.go @@ -244,7 +244,7 @@ func DepositIntervalCallback(k *Keeper, ctx sdk.Context, args []byte, query icqt for _, txn := range txs.TxResponses { req := tx.GetTxRequest{Hash: txn.TxHash} hashBytes := k.cdc.MustMarshal(&req) - _, found = k.GetReceipt(ctx, types.GetReceiptKey(zone.ChainId, txn.TxHash)) + _, found = k.GetReceipt(ctx, zone.ChainId, txn.TxHash) if found { k.Logger(ctx).Debug("Found previously handled tx. Ignoring.", "txhash", txn.TxHash) continue @@ -445,7 +445,7 @@ func DepositTxCallback(k *Keeper, ctx sdk.Context, args []byte, query icqtypes.Q return fmt.Errorf("invalid tx for query - expected %s, got %s", queryRequest.Hash, hashStr) } - _, found = k.GetReceipt(ctx, types.GetReceiptKey(zone.ChainId, hashStr)) + _, found = k.GetReceipt(ctx, zone.ChainId, hashStr) if found { k.Logger(ctx).Info("Found previously handled tx. Ignoring.", "txhash", hashStr) return nil diff --git a/x/interchainstaking/keeper/callbacks_test.go b/x/interchainstaking/keeper/callbacks_test.go index 82b999e52..d142b13a8 100644 --- a/x/interchainstaking/keeper/callbacks_test.go +++ b/x/interchainstaking/keeper/callbacks_test.go @@ -11,15 +11,19 @@ import ( "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/proto/tendermint/types" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/bech32" "github.com/cosmos/cosmos-sdk/types/query" "github.com/cosmos/cosmos-sdk/types/tx" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ibctypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types" lightclienttypes "github.com/cosmos/ibc-go/v5/modules/light-clients/07-tendermint/types" "github.com/quicksilver-zone/quicksilver/app" @@ -802,6 +806,230 @@ func (suite *KeeperTestSuite) TestHandleValideRewardsCallback() { }) } +func (suite *KeeperTestSuite) TestHandleDistributeRewardsCallback() { + suite.SetupTest() + suite.setupTestZones() + quicksilver := suite.GetQuicksilverApp(suite.chainA) + gaia := suite.GetQuicksilverApp(suite.chainB) + quicksilver.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + + ctxA := suite.chainA.GetContext() + ctxB := suite.chainB.GetContext() + vals := gaia.StakingKeeper.GetAllValidators(ctxB) + + zone, found := quicksilver.InterchainstakingKeeper.GetZone(ctxA, suite.chainB.ChainID) + suite.True(found) + params := quicksilver.InterchainstakingKeeper.GetParams(ctxA) + commisionRate := sdk.MustNewDecFromStr("0.2") + params.CommissionRate = commisionRate + quicksilver.InterchainstakingKeeper.SetParams(ctxA, params) + + prevRedemptionRate := zone.RedemptionRate + tests := []struct { + name string + zoneSetup func() + connectionSetup func() string + responseMsg func() []byte + queryMsg icqtypes.Query + check func() + pass bool + }{ + { + // delta = ratio / redemption_rate + // The original redemption rate is 1 so if ratio exceed (-0.95, 1.02) range, + // it would be kept in the boundary + name: "valid case with positive rewards and -95% < delta < 102%", + zoneSetup: func() { + balances := sdk.NewCoins( + sdk.NewCoin( + zone.LocalDenom, + math.NewInt(100_000_000), + ), + ) + err := quicksilver.MintKeeper.MintCoins(ctxA, balances) + suite.NoError(err) + qAssetAmount := quicksilver.BankKeeper.GetSupply(ctxA, zone.LocalDenom) + suite.Equal(balances[0], qAssetAmount) + + delegation := icstypes.Delegation{DelegationAddress: zone.DelegationAddress.Address, ValidatorAddress: vals[0].OperatorAddress, Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(100_000_000))} + quicksilver.InterchainstakingKeeper.SetDelegation(ctxA, &zone, delegation) + }, + connectionSetup: func() string { + channelID := quicksilver.IBCKeeper.ChannelKeeper.GenerateChannelIdentifier(ctxA) + quicksilver.IBCKeeper.ChannelKeeper.SetChannel(ctxA, icstypes.TransferPort, channelID, channeltypes.Channel{State: channeltypes.OPEN, Ordering: channeltypes.ORDERED, Counterparty: channeltypes.Counterparty{PortId: icstypes.TransferPort, ChannelId: channelID}, ConnectionHops: []string{suite.path.EndpointA.ConnectionID}}) + quicksilver.IBCKeeper.ChannelKeeper.SetNextSequenceSend(ctxA, icstypes.TransferPort, channelID, 1) + return channelID + }, + responseMsg: func() []byte { + balances := sdk.NewCoins( + sdk.NewCoin( + zone.BaseDenom, + math.NewInt(1_000_000), + ), + ) + + response := banktypes.QueryAllBalancesResponse{ + Balances: balances, + } + respbz, err := quicksilver.AppCodec().Marshal(&response) + suite.NoError(err) + return respbz + }, + queryMsg: icqtypes.Query{ChainId: suite.chainB.ChainID}, + check: func() { + zone, _ := quicksilver.InterchainstakingKeeper.GetZone(ctxA, suite.chainB.ChainID) + redemptionRate := zone.RedemptionRate + + // The ratio is calculated as: + // ratio = (total_delegate + total_unbonding + epoch_rewards) / total_q_asset + // total_delegate = total_q_asset = 100_000_000 + // total_unbonding = 0 + // epoch_rewards = balances * (1 - commission_rate) = 1_000_000 * 0.8 + // Therefore, ratio should be 1.008 + ratio := sdk.MustNewDecFromStr("1.008") + suite.Equal(ratio.Mul(prevRedemptionRate), redemptionRate) + }, + pass: true, + }, + { + name: "valid case with no rewards", + zoneSetup: func() {}, + connectionSetup: func() string { + channelID := quicksilver.IBCKeeper.ChannelKeeper.GenerateChannelIdentifier(ctxA) + quicksilver.IBCKeeper.ChannelKeeper.SetChannel(ctxA, icstypes.TransferPort, channelID, channeltypes.Channel{State: channeltypes.OPEN, Ordering: channeltypes.ORDERED, Counterparty: channeltypes.Counterparty{PortId: icstypes.TransferPort, ChannelId: channelID}, ConnectionHops: []string{suite.path.EndpointA.ConnectionID}}) + quicksilver.IBCKeeper.ChannelKeeper.SetNextSequenceSend(ctxA, icstypes.TransferPort, channelID, 1) + return channelID + }, + responseMsg: func() []byte { + response := banktypes.QueryAllBalancesResponse{ + Balances: sdk.Coins{}, + } + respbz, err := quicksilver.AppCodec().Marshal(&response) + suite.NoError(err) + return respbz + }, + queryMsg: icqtypes.Query{ChainId: suite.chainB.ChainID}, + check: func() { + zone, _ := quicksilver.InterchainstakingKeeper.GetZone(ctxA, suite.chainB.ChainID) + redemptionRate := zone.RedemptionRate + + suite.Equal(prevRedemptionRate, redemptionRate) + }, + pass: true, + }, + { + name: "invalid chainId query", + zoneSetup: func() {}, + connectionSetup: func() string { + channelID := quicksilver.IBCKeeper.ChannelKeeper.GenerateChannelIdentifier(ctxA) + quicksilver.IBCKeeper.ChannelKeeper.SetChannel(ctxA, icstypes.TransferPort, channelID, channeltypes.Channel{State: channeltypes.OPEN, Ordering: channeltypes.ORDERED, Counterparty: channeltypes.Counterparty{PortId: icstypes.TransferPort, ChannelId: channelID}, ConnectionHops: []string{suite.path.EndpointA.ConnectionID}}) + quicksilver.IBCKeeper.ChannelKeeper.SetNextSequenceSend(ctxA, icstypes.TransferPort, channelID, 1) + return channelID + }, + responseMsg: func() []byte { + balances := sdk.NewCoins( + sdk.NewCoin( + zone.BaseDenom, + math.NewInt(10_000_000), + ), + ) + + response := banktypes.QueryAllBalancesResponse{ + Balances: balances, + } + respbz, err := quicksilver.AppCodec().Marshal(&response) + suite.NoError(err) + return respbz + }, + queryMsg: icqtypes.Query{ChainId: ""}, + check: func() {}, + pass: false, + }, + { + name: "invalid response", + zoneSetup: func() {}, + connectionSetup: func() string { + channelID := quicksilver.IBCKeeper.ChannelKeeper.GenerateChannelIdentifier(ctxA) + quicksilver.IBCKeeper.ChannelKeeper.SetChannel(ctxA, icstypes.TransferPort, channelID, channeltypes.Channel{State: channeltypes.OPEN, Ordering: channeltypes.ORDERED, Counterparty: channeltypes.Counterparty{PortId: icstypes.TransferPort, ChannelId: channelID}, ConnectionHops: []string{suite.path.EndpointA.ConnectionID}}) + quicksilver.IBCKeeper.ChannelKeeper.SetNextSequenceSend(ctxA, icstypes.TransferPort, channelID, 1) + return channelID + }, + responseMsg: func() []byte { + balance := sdk.NewCoin( + zone.BaseDenom, + math.NewInt(10_000_000), + ) + respbz, err := quicksilver.AppCodec().Marshal(&balance) + suite.NoError(err) + return respbz + }, + queryMsg: icqtypes.Query{ChainId: suite.chainB.ChainID}, + check: func() {}, + pass: false, + }, + { + name: "no connection setup", + zoneSetup: func() {}, + connectionSetup: func() string { + return "" + }, + responseMsg: func() []byte { + balances := sdk.NewCoins( + sdk.NewCoin( + zone.BaseDenom, + math.NewInt(10_000_000), + ), + ) + + response := banktypes.QueryAllBalancesResponse{ + Balances: balances, + } + respbz, err := quicksilver.AppCodec().Marshal(&response) + suite.NoError(err) + return respbz + }, + queryMsg: icqtypes.Query{ChainId: suite.chainB.ChainID}, + check: func() {}, + pass: false, + }, + } + for _, test := range tests { + suite.Run(test.name, func() { + // Send coin to withdrawal address + balances := sdk.NewCoins( + sdk.NewCoin( + zone.BaseDenom, + math.NewInt(10_000_000), + ), + ) + err := gaia.MintKeeper.MintCoins(ctxB, balances) + suite.NoError(err) + addr, err := addressutils.AccAddressFromBech32(zone.WithdrawalAddress.Address, "") + suite.NoError(err) + err = gaia.BankKeeper.SendCoinsFromModuleToAccount(ctxB, minttypes.ModuleName, addr, balances) + suite.NoError(err) + + test.zoneSetup() + channelID := test.connectionSetup() + + respbz := test.responseMsg() + err = keeper.DistributeRewardsFromWithdrawAccount(quicksilver.InterchainstakingKeeper, ctxA, respbz, test.queryMsg) + if test.pass { + suite.NoError(err) + } else { + suite.Error(err) + } + + test.check() + channel, found := quicksilver.IBCKeeper.ChannelKeeper.GetChannel(ctxA, icstypes.TransferPort, channelID) + if found { + channel.State = channeltypes.CLOSED + quicksilver.IBCKeeper.ChannelKeeper.SetChannel(ctxA, icstypes.TransferPort, channelID, channel) + } + }) + } +} + func (suite *KeeperTestSuite) TestAllBalancesCallback() { suite.Run("all balances non-zero)", func() { suite.SetupTest() diff --git a/x/interchainstaking/keeper/grpc_query.go b/x/interchainstaking/keeper/grpc_query.go index ab2a52c06..2da8ac250 100644 --- a/x/interchainstaking/keeper/grpc_query.go +++ b/x/interchainstaking/keeper/grpc_query.go @@ -226,7 +226,7 @@ func (k *Keeper) TxStatus(c context.Context, req *types.QueryTxStatusRequest) (* ctx := sdk.UnwrapSDKContext(c) - txReceipt, found := k.GetReceipt(ctx, types.GetReceiptKey(req.GetChainId(), req.GetTxHash())) + txReceipt, found := k.GetReceipt(ctx, req.GetChainId(), req.GetTxHash()) if !found { return nil, status.Error(codes.NotFound, fmt.Sprintf("no receipt found matching %s", req.TxHash)) } diff --git a/x/interchainstaking/keeper/grpc_query_test.go b/x/interchainstaking/keeper/grpc_query_test.go index cf221ad94..57b2f19e4 100644 --- a/x/interchainstaking/keeper/grpc_query_test.go +++ b/x/interchainstaking/keeper/grpc_query_test.go @@ -772,7 +772,7 @@ func (suite *KeeperTestSuite) TestKeeper_ZoneWithdrawalRecords() { zone, found := icsKeeper.GetZone(ctx, suite.chainB.ChainID) suite.True(found) - distribution := []*types.Distribution{ + distributions := []*types.Distribution{ { Valoper: icsKeeper.GetValidators(ctx, suite.chainB.ChainID)[0].ValoperAddress, Amount: 10000000, @@ -788,7 +788,7 @@ func (suite *KeeperTestSuite) TestKeeper_ZoneWithdrawalRecords() { ctx, zone.ChainId, delegatorAddress, - distribution, + distributions, testAddress, sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, math.NewInt(15000000))), sdk.NewCoin(zone.LocalDenom, math.NewInt(15000000)), @@ -877,7 +877,7 @@ func (suite *KeeperTestSuite) TestKeeper_UserWithdrawalRecords() { zone, found := icsKeeper.GetZone(ctx, suite.chainB.ChainID) suite.True(found) - distribution := []*types.Distribution{ + distributions := []*types.Distribution{ { Valoper: icsKeeper.GetValidators(ctx, suite.chainB.ChainID)[0].ValoperAddress, Amount: 10000000, @@ -893,7 +893,7 @@ func (suite *KeeperTestSuite) TestKeeper_UserWithdrawalRecords() { ctx, zone.ChainId, delegatorAddress, - distribution, + distributions, testAddress, sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, math.NewInt(15000000))), sdk.NewCoin(zone.LocalDenom, math.NewInt(15000000)), @@ -970,7 +970,7 @@ func (suite *KeeperTestSuite) TestKeeper_WithdrawalRecords() { zone, found := icsKeeper.GetZone(ctx, suite.chainB.ChainID) suite.True(found) - distribution := []*types.Distribution{ + distributions := []*types.Distribution{ { Valoper: icsKeeper.GetValidators(ctx, suite.chainB.ChainID)[0].ValoperAddress, Amount: 10000000, @@ -986,7 +986,7 @@ func (suite *KeeperTestSuite) TestKeeper_WithdrawalRecords() { ctx, zone.ChainId, delegatorAddress, - distribution, + distributions, testAddress, sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, math.NewInt(15000000))), sdk.NewCoin(zone.LocalDenom, math.NewInt(15000000)), diff --git a/x/interchainstaking/keeper/ibc_packet_handlers.go b/x/interchainstaking/keeper/ibc_packet_handlers.go index a1220fde9..efcaa7909 100644 --- a/x/interchainstaking/keeper/ibc_packet_handlers.go +++ b/x/interchainstaking/keeper/ibc_packet_handlers.go @@ -542,8 +542,8 @@ func (k *Keeper) HandleTokenizedShares(ctx sdk.Context, msg sdk.Msg, sharesAmoun } for _, dist := range withdrawalRecord.Distribution { - if sharesAmount.Equal(dist.Amount) { - withdrawalRecord.Amount.Add(sharesAmount) + if equalLsmCoin(dist.Valoper, dist.Amount, sharesAmount) { + withdrawalRecord.Amount = withdrawalRecord.Amount.Add(sharesAmount) // matched amount if len(withdrawalRecord.Distribution) == len(withdrawalRecord.Amount) { // we just added the last tokens @@ -937,7 +937,7 @@ func (k *Keeper) HandleDelegate(ctx sdk.Context, msg sdk.Msg, memo string) error } } default: - receipt, found := k.GetReceipt(ctx, types.GetReceiptKey(zone.ChainId, memo)) + receipt, found := k.GetReceipt(ctx, zone.ChainId, memo) if !found { return fmt.Errorf("unable to find receipt for hash %s", memo) } @@ -1240,3 +1240,10 @@ func (*Keeper) prepareRewardsDistributionMsgs(zone types.Zone, rewards sdkmath.I Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, rewards)), } } + +func equalLsmCoin(valoper string, amount uint64, lsmAmount sdk.Coin) bool { + if strings.Contains(lsmAmount.Denom, valoper) { + return lsmAmount.Amount.Equal(sdk.NewIntFromUint64(amount)) + } + return false +} diff --git a/x/interchainstaking/keeper/ibc_packet_handlers_test.go b/x/interchainstaking/keeper/ibc_packet_handlers_test.go index dcc277202..c1912882a 100644 --- a/x/interchainstaking/keeper/ibc_packet_handlers_test.go +++ b/x/interchainstaking/keeper/ibc_packet_handlers_test.go @@ -2,10 +2,12 @@ package keeper_test import ( "context" + "errors" "fmt" "testing" "time" + lsmstakingtypes "github.com/iqlusioninc/liquidity-staking-module/x/staking/types" "github.com/stretchr/testify/require" "cosmossdk.io/math" @@ -2862,6 +2864,87 @@ func (suite *KeeperTestSuite) TestGetValidatorForToken() { } } +func (suite *KeeperTestSuite) TestHandleCompleteSend() { + testCases := []struct { + name string + message func(zone *icstypes.Zone) sdk.Msg + memo string + expectedError error + }{ + { + name: "unexpected completed send", + message: func(zone *icstypes.Zone) sdk.Msg { + return &banktypes.MsgSend{ + FromAddress: "", + ToAddress: "", + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1_000_000))), + } + }, + expectedError: errors.New("unexpected completed send (2) from to (amount: 1000000uatom)"), + }, + { + name: "From WithdrawalAddress", + message: func(zone *icstypes.Zone) sdk.Msg { + return &banktypes.MsgSend{ + FromAddress: zone.WithdrawalAddress.Address, + ToAddress: "", + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1_000_000))), + } + }, + expectedError: nil, + }, + { + name: "From DepositAddress to DelegateAddress", + message: func(zone *icstypes.Zone) sdk.Msg { + return &banktypes.MsgSend{ + FromAddress: zone.DepositAddress.Address, + ToAddress: zone.DelegationAddress.Address, + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1_000_000))), + } + }, + memo: "unbondSend/7C8B95EEE82CB63771E02EBEB05E6A80076D70B2E0A1C457F1FD1A0EF2EA961D", + expectedError: nil, + }, + { + name: "From DepositAddress", + message: func(zone *icstypes.Zone) sdk.Msg { + return &banktypes.MsgSend{ + FromAddress: zone.DelegationAddress.Address, + ToAddress: "", + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1_000_000))), + } + }, + memo: "unbondSend/7C8B95EEE82CB63771E02EBEB05E6A80076D70B2E0A1C457F1FD1A0EF2EA961D", + expectedError: errors.New("no matching withdrawal record found"), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + suite.setupTestZones() + + quicksilver := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + ctx = ctx.WithContext(context.WithValue(ctx.Context(), utils.ContextKey("connectionID"), suite.path.EndpointA.ConnectionID)) + zone, found := quicksilver.InterchainstakingKeeper.GetZone(ctx, suite.chainB.ChainID) + if !found { + suite.Fail("unable to retrieve zone for test") + } + quicksilver.InterchainstakingKeeper.IBCKeeper.ChannelKeeper.SetChannel(ctx, "transfer", "channel-0", TestChannel) + + msg := tc.message(&zone) + + err := quicksilver.InterchainstakingKeeper.HandleCompleteSend(ctx, msg, tc.memo) + if tc.expectedError != nil { + suite.Equal(tc.expectedError, err) + } else { + suite.NoError(err) + } + }) + } +} + func (suite *KeeperTestSuite) TestHandleFailedBankSend() { v1 := addressutils.GenerateValAddressForTest().String() v2 := addressutils.GenerateValAddressForTest().String() @@ -3066,3 +3149,220 @@ func (suite *KeeperTestSuite) TestHandleFailedBankSend() { }) } } + +func (suite *KeeperTestSuite) TestHandleRedeemTokens() { + tests := []struct { + name string + errs []bool + tokens func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Coin + msgs func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg + delegationRecords func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.Delegation + expectedDelegationRecords func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.Delegation + }{ + { + name: "1 record, 1 msgs, redeem success", + errs: []bool{false}, + delegationRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.Delegation { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.Delegation{ + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(vals[0]+"0x", sdk.NewInt(1000)), + Height: 1, + RedelegationEnd: 1, + }, + } + }, + tokens: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Coin { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Coin{ + sdk.NewCoin(vals[0]+"0x", sdk.NewInt(200)), + } + }, + msgs: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Msg{ + &lsmstakingtypes.MsgRedeemTokensforShares{ + DelegatorAddress: zone.DelegationAddress.Address, + Amount: sdk.NewCoin(vals[0]+"0x", sdk.NewInt(500)), + }, + } + }, + expectedDelegationRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.Delegation { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.Delegation{ + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(vals[0]+"0x", sdk.NewInt(1200)), + Height: 1, + RedelegationEnd: 1, + }, + } + }, + }, + { + name: "2 record, 2 msgs, redeem success", + errs: []bool{false, false}, + delegationRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.Delegation { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.Delegation{ + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(vals[0]+"0x", sdk.NewInt(1000)), + Height: 1, + RedelegationEnd: 1, + }, + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[1], + Amount: sdk.NewCoin(vals[1]+"1x", sdk.NewInt(1000)), + Height: 1, + RedelegationEnd: 1, + }, + } + }, + tokens: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Coin { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Coin{ + sdk.NewCoin(vals[0]+"0x", sdk.NewInt(100)), + sdk.NewCoin(vals[1]+"1x", sdk.NewInt(200)), + } + }, + msgs: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Msg{ + &lsmstakingtypes.MsgRedeemTokensforShares{ + DelegatorAddress: zone.DelegationAddress.Address, + Amount: sdk.NewCoin(vals[0]+"0x", sdk.NewInt(100)), + }, + &lsmstakingtypes.MsgRedeemTokensforShares{ + DelegatorAddress: zone.DelegationAddress.Address, + Amount: sdk.NewCoin(vals[1]+"1x", sdk.NewInt(200)), + }, + } + }, + expectedDelegationRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.Delegation { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.Delegation{ + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(vals[0]+"0x", sdk.NewInt(1100)), + Height: 1, + RedelegationEnd: 1, + }, + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[1], + Amount: sdk.NewCoin(vals[1]+"1x", sdk.NewInt(1200)), + Height: 1, + RedelegationEnd: 1, + }, + } + }, + }, + { + name: "2 record, 2 msgs, redeem failed first msg", + errs: []bool{true, false}, + delegationRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.Delegation { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.Delegation{ + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(vals[0]+"0x", sdk.NewInt(1000)), + Height: 1, + RedelegationEnd: 1, + }, + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[1], + Amount: sdk.NewCoin(vals[1]+"1x", sdk.NewInt(1000)), + Height: 1, + RedelegationEnd: 1, + }, + } + }, + tokens: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Coin { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Coin{ + sdk.NewCoin(vals[0]+"3x", sdk.NewInt(100)), + sdk.NewCoin(vals[1]+"1x", sdk.NewInt(200)), + } + }, + msgs: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Msg{ + &lsmstakingtypes.MsgRedeemTokensforShares{ + DelegatorAddress: zone.DelegationAddress.Address, + Amount: sdk.NewCoin("hello", sdk.NewInt(100)), + }, + &lsmstakingtypes.MsgRedeemTokensforShares{ + DelegatorAddress: zone.DelegationAddress.Address, + Amount: sdk.NewCoin(vals[1]+"1x", sdk.NewInt(200)), + }, + } + }, + expectedDelegationRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.Delegation { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.Delegation{ + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(vals[0]+"0x", sdk.NewInt(1000)), + Height: 1, + RedelegationEnd: 1, + }, + { + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[1], + Amount: sdk.NewCoin(vals[1]+"1x", sdk.NewInt(1200)), + Height: 1, + RedelegationEnd: 1, + }, + } + }, + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + suite.SetupTest() + suite.setupTestZones() + + quicksilver := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + ctx = ctx.WithContext(context.WithValue(ctx.Context(), utils.ContextKey("connectionID"), suite.path.EndpointA.ConnectionID)) + + zone, found := quicksilver.InterchainstakingKeeper.GetZone(ctx, suite.chainB.ChainID) + + tokens := test.tokens(ctx, quicksilver, zone) + if !found { + suite.Fail("unable to retrieve zone for test") + } + + for _, dr := range test.delegationRecords(ctx, quicksilver, zone) { + quicksilver.InterchainstakingKeeper.SetDelegation(ctx, &zone, dr) + } + for idx, msg := range test.msgs(ctx, quicksilver, zone) { + err := quicksilver.InterchainstakingKeeper.HandleRedeemTokens(ctx, msg, tokens[idx]) + if test.errs[idx] { + suite.Error(err) + } else { + suite.NoError(err) + } + } + + for _, edr := range test.expectedDelegationRecords(ctx, quicksilver, zone) { + dr, found := quicksilver.InterchainstakingKeeper.GetDelegation(ctx, &zone, edr.DelegationAddress, edr.ValidatorAddress) + suite.True(found) + suite.Equal(dr.Amount, edr.Amount) + suite.Equal(dr.Height, edr.Height) + suite.Equal(dr.RedelegationEnd, edr.RedelegationEnd) + } + }) + } +} diff --git a/x/interchainstaking/keeper/keeper.go b/x/interchainstaking/keeper/keeper.go index 919e167e6..97397249a 100644 --- a/x/interchainstaking/keeper/keeper.go +++ b/x/interchainstaking/keeper/keeper.go @@ -430,15 +430,15 @@ func (k *Keeper) SetParams(ctx sdk.Context, params types.Params) { func (k *Keeper) GetChainID(ctx sdk.Context, connectionID string) (string, error) { conn, found := k.IBCKeeper.ConnectionKeeper.GetConnection(ctx, connectionID) if !found { - return "", fmt.Errorf("invalid connection id, \"%s\" not found", connectionID) + return "", fmt.Errorf("invalid connection id, %q not found", connectionID) } clientState, found := k.IBCKeeper.ClientKeeper.GetClientState(ctx, conn.ClientId) if !found { - return "", fmt.Errorf("client id \"%s\" not found for connection \"%s\"", conn.ClientId, connectionID) + return "", fmt.Errorf("client id %q not found for connection %q", conn.ClientId, connectionID) } client, ok := clientState.(*ibctmtypes.ClientState) if !ok { - return "", fmt.Errorf("invalid client state for client \"%s\" on connection \"%s\"", conn.ClientId, connectionID) + return "", fmt.Errorf("invalid client state for client %q on connection %q", conn.ClientId, connectionID) } return client.ChainId, nil diff --git a/x/interchainstaking/keeper/msg_server_test.go b/x/interchainstaking/keeper/msg_server_test.go index 5a9fe29f7..35ff3122c 100644 --- a/x/interchainstaking/keeper/msg_server_test.go +++ b/x/interchainstaking/keeper/msg_server_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "errors" "fmt" "time" @@ -10,8 +11,10 @@ import ( capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types" connectiontypes "github.com/cosmos/ibc-go/v5/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types" + tmclienttypes "github.com/cosmos/ibc-go/v5/modules/light-clients/07-tendermint/types" "github.com/quicksilver-zone/quicksilver/utils/addressutils" icskeeper "github.com/quicksilver-zone/quicksilver/x/interchainstaking/keeper" @@ -584,3 +587,106 @@ func (suite *KeeperTestSuite) TestGovCloseChannel() { }) } } + +func (suite *KeeperTestSuite) TestGovReopenChannel() { + testCase := []struct { + name string + malleate func(suite *KeeperTestSuite) *icstypes.MsgGovReopenChannel + expecErr error + }{ + { + name: "invalid connection id", + malleate: func(suite *KeeperTestSuite) *icstypes.MsgGovReopenChannel { + return &icstypes.MsgGovReopenChannel{ + ConnectionId: "", + PortId: "", + Authority: "", + } + }, + expecErr: fmt.Errorf("unable to obtain chain id: invalid connection id, \"%s\" not found", ""), + }, + { + name: "chainID / connectsionID mismatch", + malleate: func(suite *KeeperTestSuite) *icstypes.MsgGovReopenChannel { + return &icstypes.MsgGovReopenChannel{ + ConnectionId: suite.path.EndpointA.ConnectionID, + PortId: "", + Authority: "", + } + }, + expecErr: fmt.Errorf("chainID / connectionID mismatch. Connection: %s, Port: %s", "testchain2", ""), + }, + { + name: "existing active channel", + malleate: func(suite *KeeperTestSuite) *icstypes.MsgGovReopenChannel { + k := suite.GetQuicksilverApp(suite.chainA).InterchainstakingKeeper + ctx := suite.chainA.GetContext() + channels := suite.GetQuicksilverApp(suite.chainA).IBCKeeper.ChannelKeeper.GetAllChannels(ctx) + return &icstypes.MsgGovReopenChannel{ + ConnectionId: suite.path.EndpointA.ConnectionID, + PortId: channels[0].PortId, + Authority: sdk.MustBech32ifyAddressBytes(sdk.GetConfig().GetBech32AccountAddrPrefix(), k.AccountKeeper.GetModuleAddress(govtypes.ModuleName)), + } + }, + expecErr: errors.New("existing active channel channel-7 for portID icacontroller-testchain2.delegate on connection connection-0 for owner testchain2.delegate: active channel already set for this owner"), + }, + { + name: "pass", + malleate: func(suite *KeeperTestSuite) *icstypes.MsgGovReopenChannel { + quicksilver := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + connectionID := "connection-1" + portID := "icacontroller-testchain2.delegate" + channelID := "channel-9" + + version := []*connectiontypes.Version{ + {Identifier: "1", Features: []string{"ORDER_ORDERED", "ORDER_UNORDERED"}}, + } + connectionEnd := connectiontypes.ConnectionEnd{ClientId: "09-tendermint-1", State: connectiontypes.OPEN, Versions: version} + quicksilver.IBCKeeper.ConnectionKeeper.SetConnection(ctx, connectionID, connectionEnd) + + _, f := quicksilver.IBCKeeper.ConnectionKeeper.GetConnection(ctx, connectionID) + suite.True(f) + + channelSet := channeltypes.Channel{ + State: channeltypes.TRYOPEN, + Ordering: channeltypes.NONE, + Counterparty: channeltypes.NewCounterparty(portID, channelID), + ConnectionHops: []string{connectionID}, + } + quicksilver.IBCKeeper.ChannelKeeper.SetChannel(ctx, portID, channelID, channelSet) + + quicksilver.IBCKeeper.ClientKeeper.SetClientState(ctx, connectionEnd.ClientId, &tmclienttypes.ClientState{ChainId: suite.chainB.ChainID, TrustingPeriod: time.Hour, LatestHeight: clienttypes.Height{RevisionNumber: 1, RevisionHeight: 100}}) + + return &icstypes.MsgGovReopenChannel{ + ConnectionId: connectionID, + PortId: portID, + Authority: sdk.MustBech32ifyAddressBytes(sdk.GetConfig().GetBech32AccountAddrPrefix(), quicksilver.InterchainstakingKeeper.AccountKeeper.GetModuleAddress(govtypes.ModuleName)), + } + }, + expecErr: nil, + }, + } + for _, tc := range testCase { + suite.Run(tc.name, func() { + suite.SetupTest() + suite.setupTestZones() + + msg := tc.malleate(suite) + msgSrv := icskeeper.NewMsgServerImpl(*suite.GetQuicksilverApp(suite.chainA).InterchainstakingKeeper) + ctx := suite.chainA.GetContext() + + _, err := msgSrv.GovReopenChannel(ctx, msg) + if tc.expecErr != nil { + suite.Equal(tc.expecErr.Error(), err.Error()) + return + } + suite.NoError(err) + + // Check connection for port has been set + conn, err := suite.GetQuicksilverApp(suite.chainA).InterchainstakingKeeper.GetConnectionForPort(ctx, msg.PortId) + suite.NoError(err) + suite.Equal(conn, msg.ConnectionId) + }) + } +} diff --git a/x/interchainstaking/keeper/receipt.go b/x/interchainstaking/keeper/receipt.go index 621a9e924..4a0dfde05 100644 --- a/x/interchainstaking/keeper/receipt.go +++ b/x/interchainstaking/keeper/receipt.go @@ -317,7 +317,8 @@ func (Keeper) NewReceipt(ctx sdk.Context, zone *types.Zone, sender, txhash strin } // GetReceipt returns receipt for the given key. -func (k *Keeper) GetReceipt(ctx sdk.Context, key string) (types.Receipt, bool) { +func (k *Keeper) GetReceipt(ctx sdk.Context, chainID, txHash string) (types.Receipt, bool) { + key := types.GetReceiptKey(chainID, txHash) receipt := types.Receipt{} store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixReceipt) bz := store.Get([]byte(key)) @@ -337,7 +338,8 @@ func (k *Keeper) SetReceipt(ctx sdk.Context, receipt types.Receipt) { } // DeleteReceipt delete receipt info. -func (k *Keeper) DeleteReceipt(ctx sdk.Context, key string) { +func (k *Keeper) DeleteReceipt(ctx sdk.Context, chainID, txHash string) { + key := types.GetReceiptKey(chainID, txHash) store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixReceipt) store.Delete([]byte(key)) } diff --git a/x/interchainstaking/keeper/receipt_test.go b/x/interchainstaking/keeper/receipt_test.go index 63e52ac2c..0de1c1f93 100644 --- a/x/interchainstaking/keeper/receipt_test.go +++ b/x/interchainstaking/keeper/receipt_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "fmt" "time" "cosmossdk.io/math" @@ -11,6 +12,8 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + transfertypes "github.com/cosmos/ibc-go/v5/modules/apps/transfer/types" + "github.com/quicksilver-zone/quicksilver/utils/addressutils" "github.com/quicksilver-zone/quicksilver/utils/randomutils" "github.com/quicksilver-zone/quicksilver/x/interchainstaking/types" @@ -79,7 +82,7 @@ func (suite *KeeperTestSuite) TestHandleReceiptTransactionBadRecipient() { err = icsKeeper.HandleReceiptTransaction(ctx, transaction, hash, zone) // suite.ErrorContains(err, "no sender found. Ignoring") - nilReceipt, found := icsKeeper.GetReceipt(ctx, types.GetReceiptKey(zone.ChainId, hash)) + nilReceipt, found := icsKeeper.GetReceipt(ctx, zone.ChainId, hash) suite.True(found) // check nilReceipt is found for hash suite.Equal("", nilReceipt.Sender) // check nilReceipt has empty sender suite.Nil(nilReceipt.Amount) // check nilReceipt has nil amount @@ -111,7 +114,7 @@ func (suite *KeeperTestSuite) TestHandleReceiptTransactionBadMessageType() { err = icsKeeper.HandleReceiptTransaction(ctx, transaction, hash, zone) // suite.ErrorContains(err, "no sender found. Ignoring") - nilReceipt, found := icsKeeper.GetReceipt(ctx, types.GetReceiptKey(zone.ChainId, hash)) + nilReceipt, found := icsKeeper.GetReceipt(ctx, zone.ChainId, hash) suite.True(found) // check nilReceipt is found for hash suite.Equal("", nilReceipt.Sender) // check nilReceipt has empty sender suite.Nil(nilReceipt.Amount) // check nilReceipt has nil amount @@ -181,7 +184,7 @@ func (suite *KeeperTestSuite) TestHandleReceiptTransactionBadMixedSender() { // err = icsKeeper.HandleReceiptTransaction(ctx, transaction, hash, zone) // suite.ErrorContains(err, "sender mismatch: expected") - nilReceipt, found := icsKeeper.GetReceipt(ctx, types.GetReceiptKey(zone.ChainId, hash)) + nilReceipt, found := icsKeeper.GetReceipt(ctx, zone.ChainId, hash) suite.True(found) // check nilReceipt is found for hash suite.Equal("", nilReceipt.Sender) // check nilReceipt has empty sender suite.Nil(nilReceipt.Amount) // check nilReceipt has nil amount @@ -294,13 +297,13 @@ func (suite *KeeperTestSuite) TestReceiptStore() { suite.NoError(err) suite.Equal(2, len(out)) - receipt, found := icsKeeper.GetReceipt(ctx, types.GetReceiptKey(zone.ChainId, hash1)) + receipt, found := icsKeeper.GetReceipt(ctx, zone.ChainId, hash1) suite.True(found) suite.Equal(receipt1, &receipt) now := ctx.BlockTime().Add(time.Second) receipt.Completed = &now icsKeeper.SetReceipt(ctx, receipt) - icsKeeper.DeleteReceipt(ctx, types.GetReceiptKey(zone.ChainId, hash2)) + icsKeeper.DeleteReceipt(ctx, zone.ChainId, hash2) out, err = icsKeeper.UserZoneReceipts(ctx, &zone, account1) suite.NoError(err) @@ -309,8 +312,68 @@ func (suite *KeeperTestSuite) TestReceiptStore() { icsKeeper.SetReceiptsCompleted(ctx, &zone, now, now) - receipt, found = icsKeeper.GetReceipt(ctx, types.GetReceiptKey(zone.ChainId, hash3)) + receipt, found = icsKeeper.GetReceipt(ctx, zone.ChainId, hash3) suite.True(found) suite.Equal(&now, receipt.Completed) } + +func (suite *KeeperTestSuite) TestSendTokenIBC() { + suite.Run("test", func() { + suite.SetupTest() + + // setup transfer channel + suite.path.EndpointA.ChannelConfig.Version = transfertypes.Version + suite.path.EndpointB.ChannelConfig.Version = transfertypes.Version + suite.coordinator.CreateTransferChannels(suite.path) + + suite.setupTestZones() + + quicksilver := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + + zone, found := quicksilver.InterchainstakingKeeper.GetZone(ctx, suite.chainB.ChainID) + suite.True(found) + + sender := suite.chainA.SenderAccount.GetAddress() + receiver := addressutils.GenerateAddressForTestWithPrefix("cosmos") + + amount := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) + err := quicksilver.BankKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(amount)) + suite.NoError(err) + err = quicksilver.BankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(amount)) + suite.NoError(err) + + // Try to send native token but wrong connection id on zone + wrongZone := zone + wrongZone.ConnectionId = "connection-10" + err = quicksilver.InterchainstakingKeeper.SendTokenIBC(ctx, sender, receiver, &wrongZone, amount) + suite.ErrorContains(err, "unable to find remote transfer connection") + + // Try to send the native token + err = quicksilver.InterchainstakingKeeper.SendTokenIBC(ctx, sender, receiver, &zone, amount) + suite.NoError(err) + + portID := types.TransferPort + channelID := suite.path.EndpointA.ChannelID + + ibcAmount := transfertypes.GetTransferCoin(portID, channelID, sdk.DefaultBondDenom, sdk.NewInt(100)) + + err = quicksilver.BankKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(ibcAmount)) + suite.NoError(err) + err = quicksilver.BankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(ibcAmount)) + suite.NoError(err) + + quicksilver.TransferKeeper.SetDenomTrace( + ctx, + transfertypes.DenomTrace{ + Path: fmt.Sprintf("%s/%s", portID, channelID), + BaseDenom: sdk.DefaultBondDenom, + }, + ) + + // Try to send the ibc token + err = quicksilver.InterchainstakingKeeper.SendTokenIBC(ctx, sender, receiver, &zone, ibcAmount) + suite.NoError(err) + }) +} diff --git a/x/interchainstaking/keeper/redelegation_record_test.go b/x/interchainstaking/keeper/redelegation_record_test.go index a4604df6b..1fb3662b4 100644 --- a/x/interchainstaking/keeper/redelegation_record_test.go +++ b/x/interchainstaking/keeper/redelegation_record_test.go @@ -111,6 +111,64 @@ func (suite *KeeperTestSuite) TestGCCompletedRedelegations() { suite.False(found) } +func (suite *KeeperTestSuite) TestDeleteRedelegationRecordByKey() { + quicksilver := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + + testValidatorOne := addressutils.GenerateAddressForTestWithPrefix("cosmosvaloper") + testValidatorTwo := addressutils.GenerateAddressForTestWithPrefix("cosmosvaloper") + testValidatorThree := addressutils.GenerateAddressForTestWithPrefix("cosmosvaloper") + + suite.SetupTest() + + // Currently there are 0 records + records := quicksilver.InterchainstakingKeeper.AllRedelegationRecords(ctx) + suite.Equal(0, len(records)) + + // Set 3 records + currentTime := ctx.BlockTime() + + record := types.RedelegationRecord{ + ChainId: "cosmoshub-4", + EpochNumber: 1, + Source: testValidatorOne, + Destination: testValidatorTwo, + Amount: 3000, + CompletionTime: currentTime.Add(time.Hour).UTC(), + } + quicksilver.InterchainstakingKeeper.SetRedelegationRecord(ctx, record) + + record = types.RedelegationRecord{ + ChainId: "cosmoshub-4", + EpochNumber: 1, + Source: testValidatorOne, + Destination: testValidatorThree, + Amount: 3000, + CompletionTime: currentTime.Add(-time.Hour).UTC(), + } + quicksilver.InterchainstakingKeeper.SetRedelegationRecord(ctx, record) + record = types.RedelegationRecord{ + ChainId: "cosmoshub-4", + EpochNumber: 1, + Source: testValidatorThree, + Destination: testValidatorTwo, + Amount: 3000, + CompletionTime: time.Time{}, + } + quicksilver.InterchainstakingKeeper.SetRedelegationRecord(ctx, record) + // Check set 3 records + records = quicksilver.InterchainstakingKeeper.AllRedelegationRecords(ctx) + suite.Equal(3, len(records)) + // Handle DeleteRedelegationRecordByKey for 3 records + quicksilver.InterchainstakingKeeper.IterateRedelegationRecords(ctx, func(idx int64, key []byte, redelegation types.RedelegationRecord) bool { + quicksilver.InterchainstakingKeeper.DeleteRedelegationRecordByKey(ctx, append(types.KeyPrefixRedelegationRecord, key...)) + return false + }) + // Check DeleteRedelegationRecordByKey 3 records to 0 records + records = quicksilver.InterchainstakingKeeper.AllRedelegationRecords(ctx) + suite.Equal(0, len(records)) +} + func (suite *KeeperTestSuite) TestGCCompletedUnbondings() { suite.SetupTest() suite.setupTestZones() diff --git a/x/interchainstaking/keeper/redemptions.go b/x/interchainstaking/keeper/redemptions.go index d562b0840..1d38663a1 100644 --- a/x/interchainstaking/keeper/redemptions.go +++ b/x/interchainstaking/keeper/redemptions.go @@ -66,8 +66,17 @@ func (k *Keeper) processRedemptionForLsm(ctx sdk.Context, zone *types.Zone, send for _, msg := range msgs { sdkMsgs = append(sdkMsgs, sdk.Msg(msg)) } + distributions := make([]*types.Distribution, 0) - k.AddWithdrawalRecord(ctx, zone.ChainId, sender.String(), []*types.Distribution{}, destination, sdk.Coins{}, burnAmount, hash, types.WithdrawStatusTokenize, time.Unix(0, 0), k.EpochsKeeper.GetEpochInfo(ctx, epochstypes.EpochIdentifierEpoch).CurrentEpoch) + for valoper, amount := range distribution { + newDistribution := types.Distribution{ + Valoper: valoper, + Amount: amount, + } + distributions = append(distributions, &newDistribution) + } + + k.AddWithdrawalRecord(ctx, zone.ChainId, sender.String(), distributions, destination, sdk.Coins{}, burnAmount, hash, types.WithdrawStatusTokenize, time.Unix(0, 0), k.EpochsKeeper.GetEpochInfo(ctx, epochstypes.EpochIdentifierEpoch).CurrentEpoch) return k.SubmitTx(ctx, sdkMsgs, zone.DelegationAddress, hash, zone.MessagesPerTx) } @@ -81,15 +90,15 @@ func (k *Keeper) queueRedemption( nativeTokens math.Int, burnAmount sdk.Coin, hash string, -) error { // nolint:unparam // we know that the error is always nil - distribution := make([]*types.Distribution, 0) +) error { //nolint:unparam // we know that the error is always nil + distributions := make([]*types.Distribution, 0) amount := sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, nativeTokens)) k.AddWithdrawalRecord( ctx, zone.ChainId, sender.String(), - distribution, + distributions, destination, amount, burnAmount, diff --git a/x/interchainstaking/keeper/validator_test.go b/x/interchainstaking/keeper/validator_test.go index a93578d60..93680b0b5 100644 --- a/x/interchainstaking/keeper/validator_test.go +++ b/x/interchainstaking/keeper/validator_test.go @@ -98,3 +98,48 @@ func (suite *KeeperTestSuite) TestStoreGetDeleteValidatorByConsAddr() { suite.False(found) }) } + +func (suite *KeeperTestSuite) TestGetActiveValidators() { + suite.Run("active validators", func() { + suite.SetupTest() + suite.setupTestZones() + + app := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + + zone, found := app.InterchainstakingKeeper.GetZone(ctx, suite.chainB.ChainID) + suite.True(found) + + validators := app.InterchainstakingKeeper.GetActiveValidators(ctx, "not a chain id") + suite.Len(validators, 0) + + validators = app.InterchainstakingKeeper.GetActiveValidators(ctx, zone.ChainId) + count := len(validators) + + validator1 := addressutils.GenerateValAddressForTest() + validator2 := addressutils.GenerateValAddressForTest() + + newValidator1 := types.Validator{ + ValoperAddress: validator1.String(), + CommissionRate: sdk.NewDec(5.0), + DelegatorShares: sdk.NewDec(1000.0), + VotingPower: sdk.NewInt(1000), + Status: stakingtypes.BondStatusBonded, + Score: sdk.NewDec(0), + } + newValidator2 := newValidator1 + newValidator2.ValoperAddress = validator2.String() + newValidator2.Status = stakingtypes.BondStatusUnbonded + + err := app.InterchainstakingKeeper.SetValidator(ctx, zone.ChainId, newValidator1) + suite.NoError(err) + + err = app.InterchainstakingKeeper.SetValidator(ctx, zone.ChainId, newValidator2) + suite.NoError(err) + + validators = app.InterchainstakingKeeper.GetActiveValidators(ctx, zone.ChainId) + count2 := len(validators) + + suite.Equal(count+1, count2) + }) +} diff --git a/x/interchainstaking/keeper/withdrawal_record.go b/x/interchainstaking/keeper/withdrawal_record.go index bf1900a4a..56e4b1378 100644 --- a/x/interchainstaking/keeper/withdrawal_record.go +++ b/x/interchainstaking/keeper/withdrawal_record.go @@ -26,8 +26,8 @@ func (k *Keeper) GetNextWithdrawalRecordSequence(ctx sdk.Context) (sequence uint return sequence } -func (k *Keeper) AddWithdrawalRecord(ctx sdk.Context, chainID, delegator string, distribution []*types.Distribution, recipient string, amount sdk.Coins, burnAmount sdk.Coin, hash string, status int32, completionTime time.Time, epochNumber int64) { - record := types.WithdrawalRecord{ChainId: chainID, Delegator: delegator, Distribution: distribution, Recipient: recipient, Amount: amount, Status: status, BurnAmount: burnAmount, Txhash: hash, CompletionTime: completionTime, EpochNumber: epochNumber} +func (k *Keeper) AddWithdrawalRecord(ctx sdk.Context, chainID, delegator string, distributions []*types.Distribution, recipient string, amount sdk.Coins, burnAmount sdk.Coin, hash string, status int32, completionTime time.Time, epochNumber int64) { + record := types.WithdrawalRecord{ChainId: chainID, Delegator: delegator, Distribution: distributions, Recipient: recipient, Amount: amount, Status: status, BurnAmount: burnAmount, Txhash: hash, CompletionTime: completionTime, EpochNumber: epochNumber} k.Logger(ctx).Error("addWithdrawalRecord", "record", record) k.SetWithdrawalRecord(ctx, record) } diff --git a/x/interchainstaking/keeper/zones.go b/x/interchainstaking/keeper/zones.go index a10dc14da..0dd952878 100644 --- a/x/interchainstaking/keeper/zones.go +++ b/x/interchainstaking/keeper/zones.go @@ -468,7 +468,7 @@ func (k *Keeper) RemoveZoneAndAssociatedRecords(ctx sdk.Context, chainID string) }) // remove receipts k.IterateZoneReceipts(ctx, zone, func(index int64, receiptInfo types.Receipt) (stop bool) { - k.DeleteReceipt(ctx, types.GetReceiptKey(receiptInfo.ChainId, receiptInfo.Txhash)) + k.DeleteReceipt(ctx, receiptInfo.ChainId, receiptInfo.Txhash) return false })