mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 11:17:28 +00:00 
			
		
		
		
	- Upgrade cosmos-sdk to v0.44.5 from v0.39.2 - Add Legacy Tx Endpoint for backwards compatibility - Add IBC v1.2.3 Support Co-authored-by: DracoLi <draco@dracoli.com> Co-authored-by: drklee3 <derrick@dlee.dev> Co-authored-by: denalimarsh <denalimarsh@gmail.com> Co-authored-by: Draco Li <draco@kava.io> Co-authored-by: Nick DeLuca <nickdeluca08@gmail.com> Co-authored-by: Kevin Davis <karzak@users.noreply.github.com> Co-authored-by: Denali Marsh <denali@kava.io>
		
			
				
	
	
		
			1266 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1266 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package keeper_test
 | 
						|
 | 
						|
import (
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/cosmos/cosmos-sdk/codec"
 | 
						|
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
						|
	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
 | 
						|
	govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
 | 
						|
 | 
						|
	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
 | 
						|
 | 
						|
	"github.com/kava-labs/kava/app"
 | 
						|
	// bep3types "github.com/kava-labs/kava/x/bep3/types"
 | 
						|
	// cdptypes "github.com/kava-labs/kava/x/cdp/types"
 | 
						|
 | 
						|
	"github.com/kava-labs/kava/x/committee/testutil"
 | 
						|
	"github.com/kava-labs/kava/x/committee/types"
 | 
						|
	// "github.com/kava-labs/kava/x/pricefeed"
 | 
						|
)
 | 
						|
 | 
						|
// func newCDPGenesisState(params cdptypes.Params) app.GenesisState {
 | 
						|
// 	genesis := cdptypes.DefaultGenesisState()
 | 
						|
// 	genesis.Params = params
 | 
						|
// 	return app.GenesisState{cdptypes.ModuleName: cdptypes.ModuleCdc.MustMarshalJSON(genesis)}
 | 
						|
// }
 | 
						|
 | 
						|
// func newBep3GenesisState(params bep3types.Params) app.GenesisState {
 | 
						|
// 	genesis := bep3types.DefaultGenesisState()
 | 
						|
// 	genesis.Params = params
 | 
						|
// 	return app.GenesisState{bep3types.ModuleName: bep3types.ModuleCdc.MustMarshalJSON(genesis)}
 | 
						|
// }
 | 
						|
 | 
						|
// func newPricefeedGenState(assets []string, prices []sdk.Dec) app.GenesisState {
 | 
						|
// 	if len(assets) != len(prices) {
 | 
						|
// 		panic("assets and prices must be the same length")
 | 
						|
// 	}
 | 
						|
// 	pfGenesis := pricefeed.DefaultGenesisState()
 | 
						|
 | 
						|
// 	for i := range assets {
 | 
						|
// 		pfGenesis.Params.Markets = append(
 | 
						|
// 			pfGenesis.Params.Markets,
 | 
						|
// 			pricefeed.Market{
 | 
						|
// 				MarketID: assets[i] + ":usd", BaseAsset: assets[i], QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true,
 | 
						|
// 			})
 | 
						|
// 		pfGenesis.PostedPrices = append(
 | 
						|
// 			pfGenesis.PostedPrices,
 | 
						|
// 			pricefeed.PostedPrice{
 | 
						|
// 				MarketID:      assets[i] + ":usd",
 | 
						|
// 				OracleAddress: sdk.AccAddress{},
 | 
						|
// 				Price:         prices[i],
 | 
						|
// 				Expiry:        time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
 | 
						|
// 			})
 | 
						|
// 	}
 | 
						|
// 	return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)}
 | 
						|
// }
 | 
						|
 | 
						|
// func (suite *keeperTestSuite) TestSubmitProposal() {
 | 
						|
// 	defaultCommitteeID := uint64(12)
 | 
						|
// 	normalCom := types.BaseCommittee{
 | 
						|
// 		ID:               defaultCommitteeID,
 | 
						|
// 		Description:      "This committee is for testing.",
 | 
						|
// 		Members:          suite.Addresses[:2],
 | 
						|
// 		Permissions:      []types.Permission{&types.GodPermission{}},
 | 
						|
// 		VoteThreshold:    testutil.D("0.667"),
 | 
						|
// 		ProposalDuration: time.Hour * 24 * 7,
 | 
						|
// 		TallyOption:      types.TALLY_OPTION_FIRST_PAST_THE_POST,
 | 
						|
// 	}
 | 
						|
 | 
						|
// 	noPermissionsCom := normalCom
 | 
						|
// 	noPermissionsCom.Permissions = []types.Permission{}
 | 
						|
 | 
						|
// 	paramChangePermissionsCom := normalCom
 | 
						|
// 	paramChangePermissionsCom.Permissions = []types.Permission{
 | 
						|
// 		types.paramsproposalbParamChangePermission{
 | 
						|
// 			AllowedParams: types.AllowedParams{
 | 
						|
// 				{Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyDebtThreshold)},
 | 
						|
// 				{Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyCollateralParams)},
 | 
						|
// 			},
 | 
						|
// 			AllowedCollateralParams: types.AllowedCollateralParams{
 | 
						|
// 				types.AllowedCollateralParam{
 | 
						|
// 					Type:         "bnb-a",
 | 
						|
// 					DebtLimit:    true,
 | 
						|
// 					StabilityFee: true,
 | 
						|
// 				},
 | 
						|
// 			},
 | 
						|
// 		},
 | 
						|
// 	}
 | 
						|
 | 
						|
// 	testCP := cdptypes.CollateralParams{{
 | 
						|
// 		Denom:               "bnb",
 | 
						|
// 		Type:                "bnb-a",
 | 
						|
// 		LiquidationRatio:    testutil.D("1.5"),
 | 
						|
// 		DebtLimit:           testutil.C("usdx", 1000000000000),
 | 
						|
// 		StabilityFee:        testutil.D("1.000000001547125958"), // %5 apr
 | 
						|
// 		LiquidationPenalty:  testutil.D("0.05"),
 | 
						|
// 		AuctionSize:         i(100),
 | 
						|
// 		Prefix:              0x20,
 | 
						|
// 		ConversionFactor:    i(6),
 | 
						|
// 		LiquidationMarketID: "bnb:usd",
 | 
						|
// 		SpotMarketID:        "bnb:usd",
 | 
						|
// 	}}
 | 
						|
// 	testCDPParams := cdptypes.DefaultParams()
 | 
						|
// 	testCDPParams.CollateralParams = testCP
 | 
						|
// 	testCDPParams.GlobalDebtLimit = testCP[0].DebtLimit
 | 
						|
 | 
						|
// 	newValidCP := make(cdptypes.CollateralParams, len(testCP))
 | 
						|
// 	copy(newValidCP, testCP)
 | 
						|
// 	newValidCP[0].DebtLimit = testutil.C("usdx", 500000000000)
 | 
						|
 | 
						|
// 	newInvalidCP := make(cdptypes.CollateralParams, len(testCP))
 | 
						|
// 	copy(newInvalidCP, testCP)
 | 
						|
// 	newInvalidCP[0].SpotMarketID = "btc:usd"
 | 
						|
 | 
						|
// 	testcases := []struct {
 | 
						|
// 		name        string
 | 
						|
// 		committee   types.BaseCommittee
 | 
						|
// 		pubProposal types.PubProposal
 | 
						|
// 		proposer    sdk.AccAddress
 | 
						|
// 		committeeID uint64
 | 
						|
// 		expectErr   bool
 | 
						|
// 	}{
 | 
						|
// 		{
 | 
						|
// 			name:        "normal text proposal",
 | 
						|
// 			committee:   normalCom,
 | 
						|
// 			pubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
// 			proposer:    normalCom.Members[0],
 | 
						|
// 			committeeID: normalCom.ID,
 | 
						|
// 			expectErr:   false,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:      "normal param change proposal",
 | 
						|
// 			committee: normalCom,
 | 
						|
// 			pubProposal: paramsproposal.NewParameterChangeProposal(
 | 
						|
// 				"A Title", "A description of this proposal.",
 | 
						|
// 				[]paramsproposal.ParamChange{
 | 
						|
// 					{
 | 
						|
// 						Subspace: "cdp", Key: string(cdptypes.KeyDebtThreshold), Value: string(suite.app.Codec().MustMarshalJSON(i(1000000))),
 | 
						|
// 					},
 | 
						|
// 				},
 | 
						|
// 			),
 | 
						|
// 			proposer:    normalCom.Members[0],
 | 
						|
// 			committeeID: normalCom.ID,
 | 
						|
// 			expectErr:   false,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:        "invalid proposal",
 | 
						|
// 			committee:   normalCom,
 | 
						|
// 			pubProposal: nil,
 | 
						|
// 			proposer:    normalCom.Members[0],
 | 
						|
// 			committeeID: normalCom.ID,
 | 
						|
// 			expectErr:   true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "missing committee",
 | 
						|
// 			// no committee
 | 
						|
// 			pubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
// 			proposer:    suite.Addresses[0],
 | 
						|
// 			committeeID: 0,
 | 
						|
// 			expectErr:   true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:        "not a member",
 | 
						|
// 			committee:   normalCom,
 | 
						|
// 			pubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
// 			proposer:    suite.Addresses[4],
 | 
						|
// 			committeeID: normalCom.ID,
 | 
						|
// 			expectErr:   true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:        "not enough permissions",
 | 
						|
// 			committee:   noPermissionsCom,
 | 
						|
// 			pubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
// 			proposer:    noPermissionsCom.Members[0],
 | 
						|
// 			committeeID: noPermissionsCom.ID,
 | 
						|
// 			expectErr:   true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:      "valid sub param change",
 | 
						|
// 			committee: paramChangePermissionsCom,
 | 
						|
// 			pubProposal: paramsproposal.NewParameterChangeProposal(
 | 
						|
// 				"A Title", "A description of this proposal.",
 | 
						|
// 				[]paramsproposal.ParamChange{
 | 
						|
// 					{
 | 
						|
// 						Subspace: "cdp",
 | 
						|
// 						Key:      string(cdptypes.KeyDebtThreshold),
 | 
						|
// 						Value:    string(suite.app.Codec().MustMarshalJSON(i(1000000000))),
 | 
						|
// 					},
 | 
						|
// 					{
 | 
						|
// 						Subspace: "cdp",
 | 
						|
// 						Key:      string(cdptypes.KeyCollateralParams),
 | 
						|
// 						Value:    string(suite.app.Codec().MustMarshalJSON(newValidCP)),
 | 
						|
// 					},
 | 
						|
// 				},
 | 
						|
// 			),
 | 
						|
// 			proposer:    paramChangePermissionsCom.Members[0],
 | 
						|
// 			committeeID: paramChangePermissionsCom.ID,
 | 
						|
// 			expectErr:   false,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:      "invalid sub param change permission",
 | 
						|
// 			committee: paramChangePermissionsCom,
 | 
						|
// 			pubProposal: paramsproposal.NewParameterChangeProposal(
 | 
						|
// 				"A Title", "A description of this proposal.",
 | 
						|
// 				[]paramsproposal.ParamChange{
 | 
						|
// 					{
 | 
						|
// 						Subspace: "cdp",
 | 
						|
// 						Key:      string(cdptypes.KeyDebtThreshold),
 | 
						|
// 						Value:    string(suite.app.Codec().MustMarshalJSON(i(1000000000))),
 | 
						|
// 					},
 | 
						|
// 					{
 | 
						|
// 						Subspace: "cdp",
 | 
						|
// 						Key:      string(cdptypes.KeyCollateralParams),
 | 
						|
// 						Value:    string(suite.app.Codec().MustMarshalJSON(newInvalidCP)),
 | 
						|
// 					},
 | 
						|
// 				},
 | 
						|
// 			),
 | 
						|
// 			proposer:    paramChangePermissionsCom.Members[0],
 | 
						|
// 			committeeID: paramChangePermissionsCom.ID,
 | 
						|
// 			expectErr:   true,
 | 
						|
// 		},
 | 
						|
// 	}
 | 
						|
 | 
						|
// 	for _, tc := range testcases {
 | 
						|
// 		suite.Run(tc.name, func() {
 | 
						|
// 			// Create local testApp because suite doesn't run the SetupTest function for subtests
 | 
						|
// 			tApp := app.NewTestApp()
 | 
						|
// 			keeper := tApp.GetCommitteeKeeper()
 | 
						|
// 			ctx := tApp.NewContext(true, tmproto.Header{})
 | 
						|
// 			tApp.InitializeFromGenesisStates(
 | 
						|
// 				newPricefeedGenState([]string{"bnb"}, []sdk.Dec{testutil.D("15.01")}),
 | 
						|
// 				newCDPGenesisState(testCDPParams),
 | 
						|
// 			)
 | 
						|
// 			// Cast BaseCommittee to MemberCommittee (if required) to meet Committee interface requirement
 | 
						|
// 			if tc.committee.ID == defaultCommitteeID {
 | 
						|
// 				keeper.SetCommittee(ctx, types.MustNewMemberCommittee(
 | 
						|
// 			}
 | 
						|
 | 
						|
// 			id, err := keeper.SubmitProposal(ctx, tc.proposer, tc.committeeID, tc.pubProposal)
 | 
						|
 | 
						|
// 			if tc.expectErr {
 | 
						|
// 				suite.NotNil(err)
 | 
						|
// 			} else {
 | 
						|
// 				suite.NoError(err)
 | 
						|
// 				pr, found := keeper.GetProposal(ctx, id)
 | 
						|
// 				suite.True(found)
 | 
						|
// 				suite.Equal(tc.committeeID, pr.CommitteeID)
 | 
						|
// 				suite.Equal(ctx.BlockTime().Add(tc.committee.GetProposalDuration()), pr.Deadline)
 | 
						|
// 			}
 | 
						|
// 		})
 | 
						|
// 	}
 | 
						|
// }
 | 
						|
 | 
						|
func (suite *keeperTestSuite) TestAddVote() {
 | 
						|
	memberCom := types.MustNewMemberCommittee(
 | 
						|
		1,
 | 
						|
		"This member committee is for testing.",
 | 
						|
		suite.Addresses[:2],
 | 
						|
		[]types.Permission{&types.GodPermission{}},
 | 
						|
		testutil.D("0.667"),
 | 
						|
		time.Hour*24*7,
 | 
						|
		types.TALLY_OPTION_FIRST_PAST_THE_POST,
 | 
						|
	)
 | 
						|
	tokenCom := types.MustNewTokenCommittee(
 | 
						|
		12,
 | 
						|
		"This token committee is for testing.",
 | 
						|
		suite.Addresses[:2],
 | 
						|
		[]types.Permission{&types.GodPermission{}},
 | 
						|
		testutil.D("0.4"),
 | 
						|
		time.Hour*24*7,
 | 
						|
		types.TALLY_OPTION_FIRST_PAST_THE_POST,
 | 
						|
		sdk.Dec{},
 | 
						|
		"hard",
 | 
						|
	)
 | 
						|
	nonMemberAddr := suite.Addresses[4]
 | 
						|
	firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
 | 
						|
 | 
						|
	testcases := []struct {
 | 
						|
		name       string
 | 
						|
		proposalID uint64
 | 
						|
		committee  types.Committee
 | 
						|
		voter      sdk.AccAddress
 | 
						|
		voteType   types.VoteType
 | 
						|
		voteTime   time.Time
 | 
						|
		expectErr  bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:       "normal MemberCommittee",
 | 
						|
			committee:  memberCom,
 | 
						|
			proposalID: types.DefaultNextProposalID,
 | 
						|
			voter:      memberCom.Members[0],
 | 
						|
			voteType:   types.VOTE_TYPE_YES,
 | 
						|
			expectErr:  false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:       "normal TokenCommittee",
 | 
						|
			committee:  tokenCom,
 | 
						|
			proposalID: types.DefaultNextProposalID,
 | 
						|
			voter:      nonMemberAddr,
 | 
						|
			voteType:   types.VOTE_TYPE_YES,
 | 
						|
			expectErr:  false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:       "nonexistent proposal",
 | 
						|
			committee:  memberCom,
 | 
						|
			proposalID: 9999999,
 | 
						|
			voter:      memberCom.Members[0],
 | 
						|
			voteType:   types.VOTE_TYPE_YES,
 | 
						|
			expectErr:  true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:       "proposal expired",
 | 
						|
			committee:  memberCom,
 | 
						|
			proposalID: types.DefaultNextProposalID,
 | 
						|
			voter:      memberCom.Members[0],
 | 
						|
			voteTime:   firstBlockTime.Add(memberCom.ProposalDuration),
 | 
						|
			voteType:   types.VOTE_TYPE_YES,
 | 
						|
			expectErr:  true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:       "MemberCommittee: voter not committee member",
 | 
						|
			committee:  memberCom,
 | 
						|
			proposalID: types.DefaultNextProposalID,
 | 
						|
			voter:      nonMemberAddr,
 | 
						|
			voteType:   types.VOTE_TYPE_YES,
 | 
						|
			expectErr:  true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:       "MemberCommittee: voter votes no",
 | 
						|
			committee:  memberCom,
 | 
						|
			proposalID: types.DefaultNextProposalID,
 | 
						|
			voter:      memberCom.Members[0],
 | 
						|
			voteType:   types.VOTE_TYPE_NO,
 | 
						|
			expectErr:  true,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testcases {
 | 
						|
		suite.Run(tc.name, func() {
 | 
						|
			// Create local testApp because suite doesn't run the SetupTest function for subtests
 | 
						|
			tApp := app.NewTestApp()
 | 
						|
			keeper := tApp.GetCommitteeKeeper()
 | 
						|
			ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: firstBlockTime})
 | 
						|
			tApp.InitializeFromGenesisStates()
 | 
						|
 | 
						|
			// setup the committee and proposal
 | 
						|
			keeper.SetCommittee(ctx, tc.committee)
 | 
						|
			_, err := keeper.SubmitProposal(ctx, tc.committee.GetMembers()[0], tc.committee.GetID(), govtypes.NewTextProposal("A Title", "A description of this proposal."))
 | 
						|
			suite.NoError(err)
 | 
						|
 | 
						|
			ctx = ctx.WithBlockTime(tc.voteTime)
 | 
						|
			err = keeper.AddVote(ctx, tc.proposalID, tc.voter, tc.voteType)
 | 
						|
 | 
						|
			if tc.expectErr {
 | 
						|
				suite.NotNil(err)
 | 
						|
			} else {
 | 
						|
				suite.NoError(err)
 | 
						|
				_, found := keeper.GetVote(ctx, tc.proposalID, tc.voter)
 | 
						|
				suite.True(found)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (suite *keeperTestSuite) TestTallyMemberCommitteeVotes() {
 | 
						|
	memberCom := types.MustNewMemberCommittee(
 | 
						|
		12,
 | 
						|
		"This committee is for testing.",
 | 
						|
		suite.Addresses[:5],
 | 
						|
		[]types.Permission{&types.GodPermission{}},
 | 
						|
		testutil.D("0.667"),
 | 
						|
		time.Hour*24*7,
 | 
						|
		types.TALLY_OPTION_DEADLINE,
 | 
						|
	)
 | 
						|
	var defaultProposalID uint64 = 1
 | 
						|
	firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
 | 
						|
 | 
						|
	testcases := []struct {
 | 
						|
		name              string
 | 
						|
		votes             []types.Vote
 | 
						|
		expectedVoteCount sdk.Dec
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:              "has 0 votes",
 | 
						|
			votes:             []types.Vote{},
 | 
						|
			expectedVoteCount: testutil.D("0"),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "has 1 vote",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: suite.Addresses[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
			},
 | 
						|
			expectedVoteCount: testutil.D("1"),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "has multiple votes",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: suite.Addresses[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
				{ProposalID: defaultProposalID, Voter: suite.Addresses[1], VoteType: types.VOTE_TYPE_YES},
 | 
						|
				{ProposalID: defaultProposalID, Voter: suite.Addresses[2], VoteType: types.VOTE_TYPE_YES},
 | 
						|
				{ProposalID: defaultProposalID, Voter: suite.Addresses[3], VoteType: types.VOTE_TYPE_YES},
 | 
						|
			},
 | 
						|
			expectedVoteCount: testutil.D("4"),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testcases {
 | 
						|
		// Set up test app
 | 
						|
		tApp := app.NewTestApp()
 | 
						|
		keeper := tApp.GetCommitteeKeeper()
 | 
						|
		ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: firstBlockTime})
 | 
						|
 | 
						|
		// Initialize test app with genesis state
 | 
						|
		tApp.InitializeFromGenesisStates(
 | 
						|
			committeeGenState(
 | 
						|
				tApp.AppCodec(),
 | 
						|
				[]types.Committee{memberCom},
 | 
						|
				[]types.Proposal{types.MustNewProposal(
 | 
						|
					govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
					defaultProposalID,
 | 
						|
					memberCom.GetID(),
 | 
						|
					firstBlockTime.Add(time.Hour*24*7),
 | 
						|
				)},
 | 
						|
				tc.votes,
 | 
						|
			),
 | 
						|
		)
 | 
						|
 | 
						|
		// Check that all votes are counted
 | 
						|
		currentVotes := keeper.TallyMemberCommitteeVotes(ctx, defaultProposalID)
 | 
						|
		suite.Equal(tc.expectedVoteCount, currentVotes)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (suite *keeperTestSuite) TestTallyTokenCommitteeVotes() {
 | 
						|
	tokenCom := types.MustNewTokenCommittee(
 | 
						|
		12,
 | 
						|
		"This committee is for testing.",
 | 
						|
		suite.Addresses[:5],
 | 
						|
		[]types.Permission{&types.GodPermission{}},
 | 
						|
		testutil.D("0.667"),
 | 
						|
		time.Hour*24*7,
 | 
						|
		types.TALLY_OPTION_DEADLINE,
 | 
						|
		testutil.D("0.4"),
 | 
						|
		"hard",
 | 
						|
	)
 | 
						|
	var defaultProposalID uint64 = 1
 | 
						|
	firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
 | 
						|
 | 
						|
	genAddrs := suite.Addresses[:8]                       // Genesis accounts
 | 
						|
	genCoinCounts := []int64{0, 0, 0, 10, 20, 30, 40, 50} // Genesis token balances
 | 
						|
 | 
						|
	testcases := []struct {
 | 
						|
		name                   string
 | 
						|
		votes                  []types.Vote
 | 
						|
		expectedYesVoteCount   sdk.Dec
 | 
						|
		expectedNoVoteCount    sdk.Dec
 | 
						|
		expectedTotalVoteCount sdk.Dec
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:                   "has 0 votes",
 | 
						|
			votes:                  []types.Vote{},
 | 
						|
			expectedYesVoteCount:   testutil.D("0"),
 | 
						|
			expectedNoVoteCount:    testutil.D("0"),
 | 
						|
			expectedTotalVoteCount: testutil.D("0"),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "counts token holder 'Yes' votes",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_YES}, // Token holder
 | 
						|
			},
 | 
						|
			expectedYesVoteCount:   sdk.NewDec(genCoinCounts[4]),
 | 
						|
			expectedNoVoteCount:    testutil.D("0"),
 | 
						|
			expectedTotalVoteCount: sdk.NewDec(genCoinCounts[4]),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "does not count non-token holder 'Yes' votes",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_YES}, // Token holder
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES}, // Non-token holder
 | 
						|
			},
 | 
						|
			expectedYesVoteCount:   sdk.NewDec(genCoinCounts[4]),
 | 
						|
			expectedNoVoteCount:    testutil.D("0"),
 | 
						|
			expectedTotalVoteCount: sdk.NewDec(genCoinCounts[4]),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "counts multiple 'Yes' votes from token holders",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_YES}, // Token holder
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[5], VoteType: types.VOTE_TYPE_YES}, // Token holder
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[6], VoteType: types.VOTE_TYPE_YES}, // Token holder
 | 
						|
			},
 | 
						|
			expectedYesVoteCount:   sdk.NewDec(genCoinCounts[4] + genCoinCounts[5] + genCoinCounts[6]),
 | 
						|
			expectedNoVoteCount:    testutil.D("0"),
 | 
						|
			expectedTotalVoteCount: sdk.NewDec(genCoinCounts[4] + genCoinCounts[5] + genCoinCounts[6]),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "counts token holder 'No' votes",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_NO}, // Token holder
 | 
						|
			},
 | 
						|
			expectedYesVoteCount:   testutil.D("0"),
 | 
						|
			expectedNoVoteCount:    sdk.NewDec(genCoinCounts[4]),
 | 
						|
			expectedTotalVoteCount: sdk.NewDec(genCoinCounts[4]),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "does not count non-token holder 'No' votes",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_NO}, // Token holder
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_NO}, // Non-token holder
 | 
						|
			},
 | 
						|
			expectedYesVoteCount:   testutil.D("0"),
 | 
						|
			expectedNoVoteCount:    sdk.NewDec(genCoinCounts[4]),
 | 
						|
			expectedTotalVoteCount: sdk.NewDec(genCoinCounts[4]),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "counts multiple 'No' votes from token holders",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_NO}, // Token holder
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[5], VoteType: types.VOTE_TYPE_NO}, // Token holder
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[6], VoteType: types.VOTE_TYPE_NO}, // Token holder
 | 
						|
			},
 | 
						|
			expectedYesVoteCount:   testutil.D("0"),
 | 
						|
			expectedNoVoteCount:    sdk.NewDec(genCoinCounts[4] + genCoinCounts[5] + genCoinCounts[6]),
 | 
						|
			expectedTotalVoteCount: sdk.NewDec(genCoinCounts[4] + genCoinCounts[5] + genCoinCounts[6]),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "includes token holder 'Abstain' votes in total vote count",
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultProposalID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_ABSTAIN}, // Token holder
 | 
						|
			},
 | 
						|
			expectedYesVoteCount:   testutil.D("0"),
 | 
						|
			expectedNoVoteCount:    testutil.D("0"),
 | 
						|
			expectedTotalVoteCount: sdk.NewDec(genCoinCounts[4]),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	// Convert accounts/token balances into format expected by genesis generation
 | 
						|
	var genCoins []sdk.Coins
 | 
						|
	var totalSupply sdk.Coins
 | 
						|
	for _, amount := range genCoinCounts {
 | 
						|
		userCoin := testutil.C("hard", amount)
 | 
						|
		genCoins = append(genCoins, testutil.Cs(userCoin))
 | 
						|
		totalSupply = totalSupply.Add(userCoin)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testcases {
 | 
						|
		// Set up test app
 | 
						|
		tApp := app.NewTestApp()
 | 
						|
		keeper := tApp.GetCommitteeKeeper()
 | 
						|
		ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: firstBlockTime})
 | 
						|
 | 
						|
		// Initialize test app with genesis state
 | 
						|
		tApp.InitializeFromGenesisStates(
 | 
						|
			committeeGenState(
 | 
						|
				tApp.AppCodec(),
 | 
						|
				[]types.Committee{tokenCom},
 | 
						|
				[]types.Proposal{types.MustNewProposal(
 | 
						|
					govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
					defaultProposalID,
 | 
						|
					tokenCom.GetID(),
 | 
						|
					firstBlockTime.Add(time.Hour*24*7),
 | 
						|
				)},
 | 
						|
				tc.votes,
 | 
						|
			),
 | 
						|
			bankGenState(tApp.AppCodec(), totalSupply),
 | 
						|
			app.NewFundedGenStateWithCoins(tApp.AppCodec(), genCoins, genAddrs),
 | 
						|
		)
 | 
						|
 | 
						|
		yesVotes, noVotes, currVotes, possibleVotes := keeper.TallyTokenCommitteeVotes(ctx, defaultProposalID, tokenCom.TallyDenom)
 | 
						|
 | 
						|
		// Check that all Yes votes are counted according to their weight
 | 
						|
		suite.Equal(tc.expectedYesVoteCount, yesVotes)
 | 
						|
		// Check that all No votes are counted according to their weight
 | 
						|
		suite.Equal(tc.expectedNoVoteCount, noVotes)
 | 
						|
		// Check that all non-Yes votes are counted according to their weight
 | 
						|
		suite.Equal(tc.expectedTotalVoteCount, currVotes)
 | 
						|
		// Check that possible votes equals the number of members on the committee
 | 
						|
		suite.Equal(totalSupply.AmountOf(tokenCom.GetTallyDenom()).ToDec(), possibleVotes)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (suite *keeperTestSuite) TestGetMemberCommitteeProposalResult() {
 | 
						|
	memberCom := types.MustNewMemberCommittee(
 | 
						|
 | 
						|
		12,
 | 
						|
		"This committee is for testing.",
 | 
						|
		suite.Addresses[:5],
 | 
						|
		[]types.Permission{&types.GodPermission{}},
 | 
						|
		testutil.D("0.667"),
 | 
						|
		time.Hour*24*7,
 | 
						|
		types.TALLY_OPTION_DEADLINE,
 | 
						|
	)
 | 
						|
	var defaultID uint64 = 1
 | 
						|
	firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
 | 
						|
 | 
						|
	testcases := []struct {
 | 
						|
		name           string
 | 
						|
		committee      types.Committee
 | 
						|
		votes          []types.Vote
 | 
						|
		proposalPasses bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:      "enough votes",
 | 
						|
			committee: memberCom,
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultID, Voter: suite.Addresses[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
				{ProposalID: defaultID, Voter: suite.Addresses[1], VoteType: types.VOTE_TYPE_YES},
 | 
						|
				{ProposalID: defaultID, Voter: suite.Addresses[2], VoteType: types.VOTE_TYPE_YES},
 | 
						|
				{ProposalID: defaultID, Voter: suite.Addresses[3], VoteType: types.VOTE_TYPE_YES},
 | 
						|
			},
 | 
						|
			proposalPasses: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "not enough votes",
 | 
						|
			committee: memberCom,
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultID, Voter: suite.Addresses[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
			},
 | 
						|
			proposalPasses: false,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testcases {
 | 
						|
		suite.Run(tc.name, func() {
 | 
						|
			// Create local testApp because suite doesn't run the SetupTest function for subtests
 | 
						|
			tApp := app.NewTestApp()
 | 
						|
			keeper := tApp.GetCommitteeKeeper()
 | 
						|
			ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: firstBlockTime})
 | 
						|
 | 
						|
			tApp.InitializeFromGenesisStates(
 | 
						|
				committeeGenState(
 | 
						|
					tApp.AppCodec(),
 | 
						|
					[]types.Committee{tc.committee},
 | 
						|
					[]types.Proposal{types.MustNewProposal(
 | 
						|
						govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
						defaultID,
 | 
						|
						tc.committee.GetID(),
 | 
						|
						firstBlockTime.Add(time.Hour*24*7),
 | 
						|
					)},
 | 
						|
					tc.votes,
 | 
						|
				),
 | 
						|
			)
 | 
						|
 | 
						|
			proposalPasses := keeper.GetMemberCommitteeProposalResult(ctx, defaultID, tc.committee)
 | 
						|
			suite.Equal(tc.proposalPasses, proposalPasses)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (suite *keeperTestSuite) TestGetTokenCommitteeProposalResult() {
 | 
						|
	tokenCom := types.MustNewTokenCommittee(
 | 
						|
		12,
 | 
						|
		"This committee is for testing.",
 | 
						|
		suite.Addresses[:5],
 | 
						|
		[]types.Permission{&types.GodPermission{}},
 | 
						|
		testutil.D("0.667"),
 | 
						|
		time.Hour*24*7,
 | 
						|
		types.TALLY_OPTION_DEADLINE,
 | 
						|
		testutil.D("0.4"),
 | 
						|
		"hard",
 | 
						|
	)
 | 
						|
	var defaultID uint64 = 1
 | 
						|
	firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
 | 
						|
 | 
						|
	genAddrs := suite.Addresses[:8]                       // Genesis accounts
 | 
						|
	genCoinCounts := []int64{0, 0, 0, 10, 20, 30, 40, 50} // Genesis token balances
 | 
						|
 | 
						|
	// ---------------------- Polling information ----------------------
 | 
						|
	//	150hard total token supply: 150 possible votes
 | 
						|
	//  40% quroum: 60 votes required to meet quroum
 | 
						|
	//  66.67% voting threshold: 2/3rds of votes must be Yes votes
 | 
						|
	// -----------------------------------------------------------------
 | 
						|
 | 
						|
	testcases := []struct {
 | 
						|
		name           string
 | 
						|
		committee      *types.TokenCommittee
 | 
						|
		votes          []types.Vote
 | 
						|
		proposalPasses bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:      "not enough votes to meet quroum",
 | 
						|
			committee: tokenCom,
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[7], VoteType: types.VOTE_TYPE_YES}, // Holds 50 tokens
 | 
						|
			},
 | 
						|
			proposalPasses: false, // 60 vote quroum; 50 total votes; 50 yes votes. Doesn't pass 40% quroum.
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "enough votes to meet quroum and enough Yes votes to pass voting threshold",
 | 
						|
			committee: tokenCom,
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[3], VoteType: types.VOTE_TYPE_NO},  // Holds 10 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[7], VoteType: types.VOTE_TYPE_YES}, // Holds 50 tokens
 | 
						|
			},
 | 
						|
			proposalPasses: true, // 60 vote quroum; 60 total votes; 50 Yes votes. Passes the 66.67% voting threshold.
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "enough votes to meet quroum via Abstain votes and enough Yes votes to pass voting threshold",
 | 
						|
			committee: tokenCom,
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[3], VoteType: types.VOTE_TYPE_ABSTAIN}, // Holds 10 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[7], VoteType: types.VOTE_TYPE_YES},     // Holds 50 tokens
 | 
						|
			},
 | 
						|
			proposalPasses: true, // 60 vote quroum; 60 total votes; 50 Yes votes. Passes the 66.67% voting threshold.
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "enough votes to meet quroum but not enough Yes votes to pass voting threshold",
 | 
						|
			committee: tokenCom,
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_YES}, // Holds 20 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[6], VoteType: types.VOTE_TYPE_NO},  // Holds 40 tokens
 | 
						|
			},
 | 
						|
			proposalPasses: false, // 60 vote quroum; 60 total votes; 20 Yes votes. Doesn't pass 66.67% voting threshold.
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "enough votes to pass voting threshold (multiple Yes votes, multiple No votes)",
 | 
						|
			committee: tokenCom,
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[3], VoteType: types.VOTE_TYPE_YES}, // Holds 10 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_YES}, // Holds 20 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[5], VoteType: types.VOTE_TYPE_YES}, // Holds 30 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[6], VoteType: types.VOTE_TYPE_NO},  // Holds 40 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[7], VoteType: types.VOTE_TYPE_YES}, // Holds 50 tokens
 | 
						|
			},
 | 
						|
			proposalPasses: true, // 60 vote quroum; 150 total votes; 110 Yes votes. Passes the 66.67% voting threshold.
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "not enough votes to pass voting threshold (multiple Yes votes, multiple No votes)",
 | 
						|
			committee: tokenCom,
 | 
						|
			votes: []types.Vote{
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[3], VoteType: types.VOTE_TYPE_YES}, // Holds 10 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[4], VoteType: types.VOTE_TYPE_YES}, // Holds 20 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[5], VoteType: types.VOTE_TYPE_YES}, // Holds 30 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[6], VoteType: types.VOTE_TYPE_YES}, // Holds 40 tokens
 | 
						|
				{ProposalID: defaultID, Voter: genAddrs[7], VoteType: types.VOTE_TYPE_NO},  // Holds 50 tokens
 | 
						|
			},
 | 
						|
			proposalPasses: false, // 60 vote quroum; 150 total votes; 100 Yes votes. Doesn't pass 66.67% voting threshold.
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	// Convert accounts/token balances into format expected by genesis generation
 | 
						|
	var genCoins []sdk.Coins
 | 
						|
	var totalSupply sdk.Coins
 | 
						|
	for _, amount := range genCoinCounts {
 | 
						|
		userCoin := testutil.C("hard", amount)
 | 
						|
		genCoins = append(genCoins, testutil.Cs(userCoin))
 | 
						|
		totalSupply = totalSupply.Add(userCoin)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testcases {
 | 
						|
		suite.Run(tc.name, func() {
 | 
						|
			// Create local testApp because suite doesn't run the SetupTest function for subtests
 | 
						|
			tApp := app.NewTestApp()
 | 
						|
			keeper := tApp.GetCommitteeKeeper()
 | 
						|
			ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: firstBlockTime})
 | 
						|
 | 
						|
			tApp.InitializeFromGenesisStates(
 | 
						|
				committeeGenState(
 | 
						|
					tApp.AppCodec(),
 | 
						|
					[]types.Committee{tc.committee},
 | 
						|
					[]types.Proposal{types.MustNewProposal(
 | 
						|
						govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
						defaultID,
 | 
						|
						tc.committee.GetID(),
 | 
						|
						firstBlockTime.Add(time.Hour*24*7),
 | 
						|
					)},
 | 
						|
					tc.votes,
 | 
						|
				),
 | 
						|
				bankGenState(tApp.AppCodec(), totalSupply),
 | 
						|
				app.NewFundedGenStateWithCoins(tApp.AppCodec(), genCoins, genAddrs),
 | 
						|
			)
 | 
						|
 | 
						|
			proposalPasses := keeper.GetTokenCommitteeProposalResult(ctx, defaultID, tc.committee)
 | 
						|
			suite.Equal(tc.proposalPasses, proposalPasses)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (suite *keeperTestSuite) TestCloseProposal() {
 | 
						|
	memberCom := types.MustNewMemberCommittee(
 | 
						|
		12,
 | 
						|
		"This committee is for testing.",
 | 
						|
		suite.Addresses[:5],
 | 
						|
		[]types.Permission{&types.GodPermission{}},
 | 
						|
		testutil.D("0.667"),
 | 
						|
		time.Hour*24*7,
 | 
						|
		types.TALLY_OPTION_DEADLINE,
 | 
						|
	)
 | 
						|
 | 
						|
	var proposalID uint64 = 1
 | 
						|
	firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
 | 
						|
 | 
						|
	tApp := app.NewTestApp()
 | 
						|
	keeper := tApp.GetCommitteeKeeper()
 | 
						|
	ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: firstBlockTime})
 | 
						|
 | 
						|
	tApp.InitializeFromGenesisStates(
 | 
						|
		committeeGenState(
 | 
						|
			tApp.AppCodec(),
 | 
						|
			[]types.Committee{memberCom},
 | 
						|
			[]types.Proposal{types.MustNewProposal(
 | 
						|
				govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
				proposalID,
 | 
						|
				memberCom.GetID(),
 | 
						|
				firstBlockTime.Add(time.Hour*24*7),
 | 
						|
			)},
 | 
						|
			[]types.Vote{},
 | 
						|
		),
 | 
						|
	)
 | 
						|
 | 
						|
	// Confirm proposal exists
 | 
						|
	proposal, found := keeper.GetProposal(ctx, proposalID)
 | 
						|
	suite.True(found)
 | 
						|
 | 
						|
	// Close proposal
 | 
						|
	keeper.CloseProposal(ctx, proposal, types.Passed)
 | 
						|
 | 
						|
	events := ctx.EventManager().Events()
 | 
						|
	event := events[0]
 | 
						|
	suite.Require().Equal("proposal_close", event.Type)
 | 
						|
 | 
						|
	hasProposalTallyAttr := false
 | 
						|
	for _, attr := range event.Attributes {
 | 
						|
		if string(attr.GetKey()) == "proposal_tally" {
 | 
						|
			hasProposalTallyAttr = true
 | 
						|
			valueStr := string(attr.GetValue())
 | 
						|
			suite.Contains(valueStr, "proposal_id")
 | 
						|
			suite.Contains(valueStr, "yes_votes")
 | 
						|
			suite.Contains(valueStr, "current_votes")
 | 
						|
			suite.Contains(valueStr, "possible_votes")
 | 
						|
			suite.Contains(valueStr, "vote_threshold")
 | 
						|
			suite.Contains(valueStr, "quorum")
 | 
						|
		}
 | 
						|
	}
 | 
						|
	suite.Require().True(hasProposalTallyAttr)
 | 
						|
 | 
						|
	// Confirm proposal doesn't exist
 | 
						|
	_, found = keeper.GetProposal(ctx, proposalID)
 | 
						|
	suite.False(found)
 | 
						|
}
 | 
						|
 | 
						|
func committeeGenState(cdc codec.Codec, committees []types.Committee, proposals []types.Proposal, votes []types.Vote) app.GenesisState {
 | 
						|
	gs := types.NewGenesisState(
 | 
						|
		uint64(len(proposals)+1),
 | 
						|
		committees,
 | 
						|
		proposals,
 | 
						|
		votes,
 | 
						|
	)
 | 
						|
	return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(gs)}
 | 
						|
}
 | 
						|
 | 
						|
func bankGenState(cdc codec.Codec, coins sdk.Coins) app.GenesisState {
 | 
						|
	gs := banktypes.DefaultGenesisState()
 | 
						|
	gs.Supply = coins
 | 
						|
	return app.GenesisState{banktypes.ModuleName: cdc.MustMarshalJSON(gs)}
 | 
						|
}
 | 
						|
 | 
						|
type UnregisteredPubProposal struct {
 | 
						|
	govtypes.TextProposal
 | 
						|
}
 | 
						|
 | 
						|
func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" }
 | 
						|
func (UnregisteredPubProposal) ProposalType() string  { return "unregistered" }
 | 
						|
 | 
						|
var _ types.PubProposal = &UnregisteredPubProposal{}
 | 
						|
 | 
						|
// func (suite *keeperTestSuite) TestValidatePubProposal() {
 | 
						|
 | 
						|
// 	testcases := []struct {
 | 
						|
// 		name        string
 | 
						|
// 		pubProposal types.PubProposal
 | 
						|
// 		expectErr   bool
 | 
						|
// 	}{
 | 
						|
// 		{
 | 
						|
// 			name:        "valid (text proposal)",
 | 
						|
// 			pubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
 | 
						|
// 			expectErr:   false,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "valid (param change proposal)",
 | 
						|
// 			pubProposal: paramsproposal.NewParameterChangeProposal(
 | 
						|
// 				"Change the debt limit",
 | 
						|
// 				"This proposal changes the debt limit of the cdp module.",
 | 
						|
// 				[]paramsproposal.ParamChange{{
 | 
						|
// 					Subspace: cdptypes.ModuleName,
 | 
						|
// 					Key:      string(cdptypes.KeyGlobalDebtLimit),
 | 
						|
// 					Value:    string(types.ModuleCdc.MustMarshalJSON(c("usdx", 100000000000))),
 | 
						|
// 				}},
 | 
						|
// 			),
 | 
						|
// 			expectErr: false,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:        "invalid (missing title)",
 | 
						|
// 			pubProposal: govtypes.TextProposal{Description: "A description of this proposal."},
 | 
						|
// 			expectErr:   true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:        "invalid (unregistered)",
 | 
						|
// 			pubProposal: UnregisteredPubProposal{govtypes.TextProposal{Title: "A Title", Description: "A description of this proposal."}},
 | 
						|
// 			expectErr:   true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name:        "invalid (nil)",
 | 
						|
// 			pubProposal: nil,
 | 
						|
// 			expectErr:   true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "invalid (proposal handler fails)",
 | 
						|
// 			pubProposal: paramsproposal.NewParameterChangeProposal(
 | 
						|
// 				"A Title",
 | 
						|
// 				"A description of this proposal.",
 | 
						|
// 				[]paramsproposal.ParamChange{{
 | 
						|
// 					Subspace: "nonsense-subspace",
 | 
						|
// 					Key:      "nonsense-key",
 | 
						|
// 					Value:    "nonsense-value",
 | 
						|
// 				}},
 | 
						|
// 			),
 | 
						|
// 			expectErr: true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "invalid (proposal handler panics)",
 | 
						|
// 			pubProposal: paramsproposal.NewParameterChangeProposal(
 | 
						|
// 				"A Title",
 | 
						|
// 				"A description of this proposal.",
 | 
						|
// 				[]paramsproposal.ParamChange{{
 | 
						|
// 					Subspace: cdptypes.ModuleName,
 | 
						|
// 					Key:      "nonsense-key", // a valid Subspace but invalid Key will trigger a panic in the paramchange propsal handler
 | 
						|
// 					Value:    "nonsense-value",
 | 
						|
// 				}},
 | 
						|
// 			),
 | 
						|
// 			expectErr: true,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "invalid (proposal handler fails - invalid json)",
 | 
						|
// 			pubProposal: paramsproposal.NewParameterChangeProposal(
 | 
						|
// 				"A Title",
 | 
						|
// 				"A description of this proposal.",
 | 
						|
// 				[]paramsproposal.ParamChange{{
 | 
						|
// 					Subspace: cdptypes.ModuleName,
 | 
						|
// 					Key:      string(cdptypes.KeyGlobalDebtLimit),
 | 
						|
// 					Value:    `{"denom": "usdx",`,
 | 
						|
// 				}},
 | 
						|
// 			),
 | 
						|
// 			expectErr: true,
 | 
						|
// 		},
 | 
						|
// 	}
 | 
						|
 | 
						|
// 	for _, tc := range testcases {
 | 
						|
// 		suite.Run(tc.name, func() {
 | 
						|
// 			err := suite.keeper.ValidatePubProposal(suite.ctx, tc.pubProposal)
 | 
						|
// 			if tc.expectErr {
 | 
						|
// 				suite.NotNil(err)
 | 
						|
// 			} else {
 | 
						|
// 				suite.NoError(err)
 | 
						|
// 			}
 | 
						|
// 		})
 | 
						|
// 	}
 | 
						|
// }
 | 
						|
 | 
						|
// func (suite *keeperTestSuite) TestProcessProposals() {
 | 
						|
 | 
						|
// 	firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
 | 
						|
 | 
						|
// 	genAddrs := suite.Addresses[:4]      // Genesis accounts
 | 
						|
// 	genCoinCounts := []int64{1, 1, 1, 1} // Genesis token balances
 | 
						|
// 	// Convert accounts/token balances into format expected by genesis generation
 | 
						|
// 	var genCoins []sdk.Coins
 | 
						|
// 	var totalSupply sdk.Coins
 | 
						|
// 	for _, amount := range genCoinCounts {
 | 
						|
// 		userCoin := testutil.C("hard", amount)
 | 
						|
// 		genCoins = append(genCoins, testutil.Cs(userCoin))
 | 
						|
// 		totalSupply = totalSupply.Add(userCoin)
 | 
						|
// 	}
 | 
						|
 | 
						|
// 	// Set up committees
 | 
						|
// 	committees := []types.Committee{
 | 
						|
// 		// 	1. FPTP MemberCommmittee
 | 
						|
// 		types.MustNewMemberCommittee(
 | 
						|
 | 
						|
// 			1,
 | 
						|
// 			"FTPT MemberCommittee",
 | 
						|
// 			genAddrs,
 | 
						|
// 			[]types.Permission{&types.GodPermission{}},
 | 
						|
// 			testutil.D("0.667"),
 | 
						|
// 			time.Hour*24*7,
 | 
						|
// 			types.TALLY_OPTION_FIRST_PAST_THE_POST,
 | 
						|
// 		),
 | 
						|
// 		// 	2. FPTP TokenCommittee
 | 
						|
// 		types.MustNewTokenCommittee(
 | 
						|
 | 
						|
// 			2,
 | 
						|
// 			"FTPT TokenCommittee",
 | 
						|
// 			genAddrs,
 | 
						|
// 			[]types.Permission{&types.GodPermission{}},
 | 
						|
// 			testutil.D("0.667"),
 | 
						|
// 			time.Hour*24*7,
 | 
						|
// 			types.TALLY_OPTION_FIRST_PAST_THE_POST,
 | 
						|
// 			testutil.D("0.30"),
 | 
						|
// 			"hard",
 | 
						|
// 		),
 | 
						|
// 		// 	3. Deadline MemberCommmittee
 | 
						|
// 		types.MustNewMemberCommittee(
 | 
						|
 | 
						|
// 			3,
 | 
						|
// 			"Deadline MemberCommittee",
 | 
						|
// 			genAddrs,
 | 
						|
// 			[]types.Permission{&types.GodPermission{}},
 | 
						|
// 			testutil.D("0.667"),
 | 
						|
// 			time.Hour*24*7,
 | 
						|
// 			types.TALLY_OPTION_DEADLINE,
 | 
						|
// 		),
 | 
						|
// 		// 	4. Deadline TokenCommittee
 | 
						|
// 		types.MustNewTokenCommittee(
 | 
						|
// 			4,
 | 
						|
// 			"Deadline TokenCommittee",
 | 
						|
// 			genAddrs,
 | 
						|
// 			[]types.Permission{&types.GodPermission{}},
 | 
						|
// 			testutil.D("0.667"),
 | 
						|
// 			time.Hour*24*7,
 | 
						|
// 			types.TALLY_OPTION_DEADLINE,
 | 
						|
// 			testutil.D("0.30"),
 | 
						|
// 			"hard",
 | 
						|
// 		),
 | 
						|
// 		// 	5. PTP MemberCommmittee without permissions
 | 
						|
// 		types.MustNewMemberCommittee(
 | 
						|
 | 
						|
// 			5,
 | 
						|
// 			"FTPT MemberCommittee without permissions",
 | 
						|
// 			genAddrs,
 | 
						|
// 			nil,
 | 
						|
// 			testutil.D("0.667"),
 | 
						|
// 			time.Hour*24*7,
 | 
						|
// 			types.TALLY_OPTION_FIRST_PAST_THE_POST,
 | 
						|
// 		),
 | 
						|
// 	}
 | 
						|
 | 
						|
// 	// Set up proposals that correspond 1:1 with each committee
 | 
						|
// 	proposals := []types.Proposal{
 | 
						|
// 		types.MustNewProposal(
 | 
						|
// 			govtypes.NewTextProposal("Proposal 1", "This proposal is for the FPTP MemberCommmittee."),
 | 
						|
// 			1,
 | 
						|
// 			1,
 | 
						|
// 			firstBlockTime.Add(7*24*time.Hour),
 | 
						|
// 		),
 | 
						|
// 		types.MustNewProposal(
 | 
						|
// 			govtypes.NewTextProposal("Proposal 2", "This proposal is for the FPTP TokenCommittee."),
 | 
						|
// 			2,
 | 
						|
// 			2,
 | 
						|
 | 
						|
// 			firstBlockTime.Add(7*24*time.Hour),
 | 
						|
// 		),
 | 
						|
// 		types.MustNewProposal(
 | 
						|
// 			govtypes.NewTextProposal("Proposal 3", "This proposal is for the Deadline MemberCommmittee."),
 | 
						|
// 			3,
 | 
						|
// 			3,
 | 
						|
 | 
						|
// 			firstBlockTime.Add(7*24*time.Hour),
 | 
						|
// 		),
 | 
						|
// 		types.MustNewProposal(
 | 
						|
// 			govtypes.NewTextProposal("Proposal 4", "This proposal is for the Deadline TokenCommittee."),
 | 
						|
// 			4,
 | 
						|
// 			4,
 | 
						|
 | 
						|
// 			firstBlockTime.Add(7*24*time.Hour),
 | 
						|
// 		),
 | 
						|
// 		types.MustNewProposal(
 | 
						|
// 			govtypes.NewTextProposal("Proposal 5", "This proposal is for the FPTP MemberCommmittee without permissions."),
 | 
						|
// 			5,
 | 
						|
// 			5,
 | 
						|
// 			firstBlockTime.Add(7*24*time.Hour),
 | 
						|
// 		),
 | 
						|
// 	}
 | 
						|
 | 
						|
// 	// Each test case targets 1 committee/proposal via targeted votes
 | 
						|
// 	testcases := []struct {
 | 
						|
// 		name                             string
 | 
						|
// 		ID                               uint64
 | 
						|
// 		votes                            []types.Vote
 | 
						|
// 		expectedToCompleteBeforeDeadline bool
 | 
						|
// 		expectedOutcome                  types.ProposalOutcome
 | 
						|
// 	}{
 | 
						|
// 		{
 | 
						|
// 			name: "FPTP MemberCommittee proposal does not have enough votes to pass",
 | 
						|
// 			ID:   1,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 1, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedToCompleteBeforeDeadline: false,
 | 
						|
// 			expectedOutcome:                  types.Failed,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "FPTP MemberCommittee proposal has enough votes to pass before deadline",
 | 
						|
// 			ID:   1,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 1, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 1, Voter: genAddrs[1], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 1, Voter: genAddrs[2], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedToCompleteBeforeDeadline: true,
 | 
						|
// 			expectedOutcome:                  types.Passed,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "FPTP TokenCommittee proposal does not have enough votes to pass",
 | 
						|
// 			ID:   2,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 2, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedToCompleteBeforeDeadline: false,
 | 
						|
// 			expectedOutcome:                  types.Failed,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "FPTP TokenCommittee proposal has enough votes to pass before deadline",
 | 
						|
// 			ID:   2,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 2, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 2, Voter: genAddrs[1], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 2, Voter: genAddrs[2], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedToCompleteBeforeDeadline: true,
 | 
						|
// 			expectedOutcome:                  types.Passed,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "Deadline MemberCommittee proposal with enough votes to pass only passes after deadline",
 | 
						|
// 			ID:   3,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 3, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 3, Voter: genAddrs[1], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 3, Voter: genAddrs[2], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedOutcome: types.Passed,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "Deadline MemberCommittee proposal doesn't have enough votes to pass",
 | 
						|
// 			ID:   3,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 3, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedOutcome: types.Failed,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "Deadline TokenCommittee proposal with enough votes to pass only passes after deadline",
 | 
						|
// 			ID:   4,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 4, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 4, Voter: genAddrs[1], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 4, Voter: genAddrs[2], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedOutcome: types.Passed,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "Deadline TokenCommittee proposal doesn't have enough votes to pass",
 | 
						|
// 			ID:   4,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 4, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedOutcome: types.Failed,
 | 
						|
// 		},
 | 
						|
// 		{
 | 
						|
// 			name: "FPTP MemberCommittee doesn't have permissions to enact passed proposal",
 | 
						|
// 			ID:   5,
 | 
						|
// 			votes: []types.Vote{
 | 
						|
// 				{ProposalID: 5, Voter: genAddrs[0], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 5, Voter: genAddrs[1], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 				{ProposalID: 5, Voter: genAddrs[2], VoteType: types.VOTE_TYPE_YES},
 | 
						|
// 			},
 | 
						|
// 			expectedToCompleteBeforeDeadline: true,
 | 
						|
// 			expectedOutcome:                  types.Invalid,
 | 
						|
// 		},
 | 
						|
// 	}
 | 
						|
 | 
						|
// 	for _, tc := range testcases {
 | 
						|
// 		suite.Run(tc.name, func() {
 | 
						|
// 			// Create local testApp because suite doesn't run the SetupTest function for subtests
 | 
						|
// 			tApp := app.NewTestApp()
 | 
						|
// 			keeper := tApp.GetCommitteeKeeper()
 | 
						|
// 			ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: firstBlockTime})
 | 
						|
 | 
						|
// 			// Initialize all committees, proposals, and votes via Genesis
 | 
						|
// 			tApp.InitializeFromGenesisStates(
 | 
						|
// 				committeeGenState(tApp.AppCodec(), committees, proposals, tc.votes),
 | 
						|
// 				bankGenState(tApp.AppCodec(), totalSupply),
 | 
						|
// 				app.NewFundedGenStateWithCoins(tApp.AppCodec(), genCoins, genAddrs),
 | 
						|
// 			)
 | 
						|
 | 
						|
// 			// Load committee from the store
 | 
						|
// 			committee, found := keeper.GetCommittee(ctx, tc.ID)
 | 
						|
// 			suite.True(found)
 | 
						|
 | 
						|
// 			// Process proposals
 | 
						|
// 			ctx = ctx.WithBlockTime(firstBlockTime)
 | 
						|
// 			keeper.ProcessProposals(ctx)
 | 
						|
 | 
						|
// 			// Fetch proposal and votes from the store
 | 
						|
// 			votes := getProposalVoteMap(keeper, ctx)
 | 
						|
// 			proposal, found := keeper.GetProposal(ctx, tc.ID)
 | 
						|
 | 
						|
// 			if committee.GetTallyOption() == types.TALLY_OPTION_FIRST_PAST_THE_POST {
 | 
						|
// 				if tc.expectedToCompleteBeforeDeadline {
 | 
						|
// 					suite.False(found)
 | 
						|
// 					suite.Empty(votes[tc.ID])
 | 
						|
 | 
						|
// 					// Check proposal outcome
 | 
						|
// 					outcome, err := getProposalOutcome(tc.ID, ctx.EventManager().Events(), tApp.LegacyAmino())
 | 
						|
// 					suite.NoError(err)
 | 
						|
// 					suite.Equal(tc.expectedOutcome, outcome)
 | 
						|
// 					return
 | 
						|
// 				} else {
 | 
						|
// 					suite.True(found)
 | 
						|
// 					suite.NotEmpty(votes[tc.ID])
 | 
						|
// 				}
 | 
						|
// 			}
 | 
						|
 | 
						|
// 			// Move block time to deadline
 | 
						|
// 			ctx = ctx.WithBlockTime(proposal.Deadline)
 | 
						|
// 			keeper.ProcessProposals(ctx)
 | 
						|
 | 
						|
// 			// Fetch proposal and votes from the store
 | 
						|
// 			votes = getProposalVoteMap(keeper, ctx)
 | 
						|
// 			proposal, found = keeper.GetProposal(ctx, tc.ID)
 | 
						|
// 			suite.False(found)
 | 
						|
// 			suite.Empty(votes[proposal.ID])
 | 
						|
 | 
						|
// 			// Check proposal outcome
 | 
						|
// 			outcome, err := getProposalOutcome(tc.ID, ctx.EventManager().Events(), tApp.LegacyAmino())
 | 
						|
// 			suite.NoError(err)
 | 
						|
// 			suite.Equal(tc.expectedOutcome, outcome)
 | 
						|
// 		})
 | 
						|
// 	}
 | 
						|
// }
 | 
						|
 | 
						|
// // getProposalOutcome checks the outcome of a proposal via a `proposal_close` event whose `proposal_id`
 | 
						|
// // matches argument proposalID
 | 
						|
// func getProposalOutcome(proposalID uint64, events sdk.Events, cdc *codec.LegacyAmino) (types.ProposalOutcome, error) {
 | 
						|
// 	// Marshal proposal ID to match against event attribute
 | 
						|
// 	x, _ := cdc.MarshalJSON(proposalID)
 | 
						|
// 	marshaledID := x[1 : len(x)-1]
 | 
						|
 | 
						|
// 	for _, event := range events {
 | 
						|
// 		if event.Type == types.EventTypeProposalClose {
 | 
						|
// 			var proposalOutcome types.ProposalOutcome
 | 
						|
// 			correctProposal := false
 | 
						|
// 			for _, attribute := range event.Attributes {
 | 
						|
// 				// Only get outcome of specific proposal
 | 
						|
// 				if bytes.Compare(attribute.GetKey(), []byte("proposal_id")) == 0 {
 | 
						|
// 					if bytes.Compare(attribute.GetValue(), marshaledID) == 0 {
 | 
						|
// 						correctProposal = true
 | 
						|
// 					}
 | 
						|
// 				}
 | 
						|
// 				// Match event attribute bytes to marshaled outcome
 | 
						|
// 				if bytes.Compare(attribute.GetKey(), []byte(types.AttributeKeyProposalOutcome)) == 0 {
 | 
						|
// 					outcome, err := types.MatchMarshaledOutcome(attribute.GetValue(), cdc)
 | 
						|
// 					if err != nil {
 | 
						|
// 						return 0, err
 | 
						|
// 					}
 | 
						|
// 					proposalOutcome = outcome
 | 
						|
// 				}
 | 
						|
// 			}
 | 
						|
// 			// If this is the desired proposal, return the outcome
 | 
						|
// 			if correctProposal {
 | 
						|
// 				return proposalOutcome, nil
 | 
						|
// 			}
 | 
						|
// 		}
 | 
						|
// 	}
 | 
						|
// 	return 0, nil
 | 
						|
// }
 |