From fc850525220c515cc5c8b90befe6b9ff17ee8148 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 2 Jun 2021 11:03:25 -0600 Subject: [PATCH] add community multi-spend proposal (#915) * feat: add community multi-spend proposal type * feat: add handler for community multi-spend proposals * chore: register new community multi-spend proposal * feat: define client for community multi-spend proposal * fix typos in example cli json * fix: register now proposal type with module codec * fix: register community multi-spend proposal with gov router, not committee * fix: define kavadist keeper before referencing it * nit: include deposit in example proposal * nit: update comment * nit: fix error codes * nit: update comments --- app/app.go | 19 ++-- x/kavadist/alias.go | 91 ++++++++++--------- x/kavadist/client/cli/tx.go | 93 ++++++++++++++++++++ x/kavadist/client/cli/utils.go | 36 ++++++++ x/kavadist/client/proposal_handler.go | 13 +++ x/kavadist/client/rest/rest.go | 36 ++++++++ x/kavadist/client/rest/utils.go | 21 +++++ x/kavadist/handler.go | 15 ++++ x/kavadist/keeper/keeper.go | 18 ++-- x/kavadist/keeper/proposal_handler.go | 23 +++++ x/kavadist/types/codec.go | 1 + x/kavadist/types/errors.go | 11 +++ x/kavadist/types/expected_keepers.go | 5 ++ x/kavadist/types/proposal.go | 120 ++++++++++++++++++++++++++ 14 files changed, 450 insertions(+), 52 deletions(-) create mode 100644 x/kavadist/client/cli/tx.go create mode 100644 x/kavadist/client/cli/utils.go create mode 100644 x/kavadist/client/proposal_handler.go create mode 100644 x/kavadist/client/rest/utils.go create mode 100644 x/kavadist/keeper/proposal_handler.go create mode 100644 x/kavadist/types/errors.go create mode 100644 x/kavadist/types/proposal.go diff --git a/app/app.go b/app/app.go index e665c8e3..5293b761 100644 --- a/app/app.go +++ b/app/app.go @@ -68,7 +68,7 @@ var ( distr.AppModuleBasic{}, gov.NewAppModuleBasic( paramsclient.ProposalHandler, distr.ProposalHandler, committee.ProposalHandler, - upgradeclient.ProposalHandler, + upgradeclient.ProposalHandler, kavadist.ProposalHandler, ), params.AppModuleBasic{}, crisis.AppModuleBasic{}, @@ -300,6 +300,14 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio committeeGovRouter, app.paramsKeeper, ) + app.kavadistKeeper = kavadist.NewKeeper( + app.cdc, + keys[kavadist.StoreKey], + kavadistSubspace, + app.supplyKeeper, + app.distrKeeper, + app.ModuleAccountAddrs(), + ) // create gov keeper with router govRouter := gov.NewRouter() @@ -308,7 +316,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)). AddRoute(upgrade.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.upgradeKeeper)). - AddRoute(committee.RouterKey, committee.NewProposalHandler(app.committeeKeeper)) + AddRoute(committee.RouterKey, committee.NewProposalHandler(app.committeeKeeper)). + AddRoute(kavadist.RouterKey, kavadist.NewCommunityPoolMultiSpendProposalHandler(app.kavadistKeeper)) app.govKeeper = gov.NewKeeper( app.cdc, keys[gov.StoreKey], @@ -365,12 +374,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio app.pricefeedKeeper, app.auctionKeeper, ) - app.kavadistKeeper = kavadist.NewKeeper( - app.cdc, - keys[kavadist.StoreKey], - kavadistSubspace, - app.supplyKeeper, - ) app.incentiveKeeper = incentive.NewKeeper( app.cdc, keys[incentive.StoreKey], diff --git a/x/kavadist/alias.go b/x/kavadist/alias.go index c87a02b1..747868ee 100644 --- a/x/kavadist/alias.go +++ b/x/kavadist/alias.go @@ -1,56 +1,69 @@ +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/kava-labs/kava/x/kavadist/types +// ALIASGEN: github.com/kava-labs/kava/x/kavadist/keeper +// ALIASGEN: github.com/kava-labs/kava/x/kavadist/client package kavadist -// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) - import ( + "github.com/kava-labs/kava/x/kavadist/client" "github.com/kava-labs/kava/x/kavadist/keeper" "github.com/kava-labs/kava/x/kavadist/types" ) const ( - AttributeKeyInflation = types.AttributeKeyInflation - AttributeKeyStatus = types.AttributeKeyStatus - AttributeValueInactive = types.AttributeValueInactive - DefaultParamspace = types.DefaultParamspace - EventTypeKavaDist = types.EventTypeKavaDist - KavaDistMacc = types.KavaDistMacc - ModuleName = types.ModuleName - QuerierRoute = types.QuerierRoute - QueryGetBalance = types.QueryGetBalance - QueryGetParams = types.QueryGetParams - RouterKey = types.RouterKey - StoreKey = types.StoreKey + EventTypeKavaDist = types.EventTypeKavaDist + AttributeKeyInflation = types.AttributeKeyInflation + AttributeKeyStatus = types.AttributeKeyStatus + AttributeValueInactive = types.AttributeValueInactive + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + KavaDistMacc = types.KavaDistMacc + ProposalTypeCommunityPoolMultiSpend = types.ProposalTypeCommunityPoolMultiSpend + QueryGetParams = types.QueryGetParams + QueryGetBalance = types.QueryGetBalance ) var ( - // function aliases - NewKeeper = keeper.NewKeeper - NewQuerier = keeper.NewQuerier - DefaultGenesisState = types.DefaultGenesisState - DefaultParams = types.DefaultParams - NewGenesisState = types.NewGenesisState - NewParams = types.NewParams - NewPeriod = types.NewPeriod - ParamKeyTable = types.ParamKeyTable - RegisterCodec = types.RegisterCodec + // functions aliases + RegisterCodec = types.RegisterCodec + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + NewPeriod = types.NewPeriod + NewParams = types.NewParams + DefaultParams = types.DefaultParams + ParamKeyTable = types.ParamKeyTable + NewCommunityPoolMultiSpendProposal = types.NewCommunityPoolMultiSpendProposal + NewKeeper = keeper.NewKeeper + HandleCommunityPoolMultiSpendProposal = keeper.HandleCommunityPoolMultiSpendProposal + NewQuerier = keeper.NewQuerier // variable aliases - CurrentDistPeriodKey = types.CurrentDistPeriodKey - DefaultActive = types.DefaultActive - DefaultPeriods = types.DefaultPeriods - DefaultPreviousBlockTime = types.DefaultPreviousBlockTime - GovDenom = types.GovDenom - KeyActive = types.KeyActive - KeyPeriods = types.KeyPeriods - ModuleCdc = types.ModuleCdc - PreviousBlockTimeKey = types.PreviousBlockTimeKey + ModuleCdc = types.ModuleCdc + ErrInvalidProposalAmount = types.ErrInvalidProposalAmount + ErrEmptyProposalRecipient = types.ErrEmptyProposalRecipient + CurrentDistPeriodKey = types.CurrentDistPeriodKey + PreviousBlockTimeKey = types.PreviousBlockTimeKey + KeyActive = types.KeyActive + KeyPeriods = types.KeyPeriods + DefaultActive = types.DefaultActive + DefaultPeriods = types.DefaultPeriods + DefaultPreviousBlockTime = types.DefaultPreviousBlockTime + GovDenom = types.GovDenom + ProposalHandler = client.ProposalHandler ) type ( - Keeper = keeper.Keeper - GenesisState = types.GenesisState - Params = types.Params - Period = types.Period - Periods = types.Periods - SupplyKeeper = types.SupplyKeeper + GenesisState = types.GenesisState + Params = types.Params + Period = types.Period + Periods = types.Periods + CommunityPoolMultiSpendProposal = types.CommunityPoolMultiSpendProposal + MultiSpendRecipient = types.MultiSpendRecipient + MultiSpendRecipients = types.MultiSpendRecipients + Keeper = keeper.Keeper ) diff --git a/x/kavadist/client/cli/tx.go b/x/kavadist/client/cli/tx.go new file mode 100644 index 00000000..b481051a --- /dev/null +++ b/x/kavadist/client/cli/tx.go @@ -0,0 +1,93 @@ +package cli + +import ( + "bufio" + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov" + + "github.com/kava-labs/kava/x/kavadist/types" +) + +// GetCmdSubmitProposal implements the command to submit a community-pool multi-spend proposal +func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "community-pool-multi-spend [proposal-file]", + Args: cobra.ExactArgs(1), + Short: "Submit a community pool multi-spend proposal", + Long: strings.TrimSpace( + fmt.Sprintf(`Submit a community pool multi-spend proposal along with an initial deposit. +The proposal details must be supplied via a JSON file. + +Example: +$ %s tx gov submit-proposal community-pool-multi-spend --from= + +Where proposal.json contains: + +{ + "title": "Community Pool Multi-Spend", + "description": "Pay many users some KAVA!", + "recipient_list": [ + { + "address": "kava1mz2003lathm95n5vnlthmtfvrzrjkrr53j4464", + "amount": [ + { + "denom": "ukava", + "amount": "1000000" + } + ] + }, + { + "address": "kava1zqezafa0luyetvtj8j67g336vaqtuudnsjq7vm", + "amount": [ + { + "denom": "ukava", + "amount": "1000000" + } + ] + } + ], + "deposit": [ + { + "denom": "ukava", + "amount": "1000000000" + } + ] +} +`, + version.ClientName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + proposal, err := ParseCommunityPoolMultiSpendProposalJSON(cdc, args[0]) + if err != nil { + return err + } + + from := cliCtx.GetFromAddress() + content := types.NewCommunityPoolMultiSpendProposal(proposal.Title, proposal.Description, proposal.RecipientList) + + msg := gov.NewMsgSubmitProposal(content, proposal.Deposit, from) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return cmd +} diff --git a/x/kavadist/client/cli/utils.go b/x/kavadist/client/cli/utils.go new file mode 100644 index 00000000..e117e39d --- /dev/null +++ b/x/kavadist/client/cli/utils.go @@ -0,0 +1,36 @@ +package cli + +import ( + "io/ioutil" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/kavadist/types" +) + +type ( + // CommunityPoolMultiSpendProposalJSON defines a CommunityPoolMultiSpendProposal with a deposit + CommunityPoolMultiSpendProposalJSON struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + RecipientList types.MultiSpendRecipients `json:"recipient_list" yaml:"recipient_list"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + } +) + +// ParseCommunityPoolMultiSpendProposalJSON reads and parses a CommunityPoolMultiSpendProposalJSON from a file. +func ParseCommunityPoolMultiSpendProposalJSON(cdc *codec.Codec, proposalFile string) (CommunityPoolMultiSpendProposalJSON, error) { + proposal := CommunityPoolMultiSpendProposalJSON{} + + contents, err := ioutil.ReadFile(proposalFile) + if err != nil { + return proposal, err + } + + if err := cdc.UnmarshalJSON(contents, &proposal); err != nil { + return proposal, err + } + + return proposal, nil +} diff --git a/x/kavadist/client/proposal_handler.go b/x/kavadist/client/proposal_handler.go new file mode 100644 index 00000000..5cc1ae75 --- /dev/null +++ b/x/kavadist/client/proposal_handler.go @@ -0,0 +1,13 @@ +package client + +import ( + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + + "github.com/kava-labs/kava/x/kavadist/client/cli" + "github.com/kava-labs/kava/x/kavadist/client/rest" +) + +// community-pool multi-spend proposal handler +var ( + ProposalHandler = govclient.NewProposalHandler(cli.GetCmdSubmitProposal, rest.ProposalRESTHandler) +) diff --git a/x/kavadist/client/rest/rest.go b/x/kavadist/client/rest/rest.go index 2e9db311..fa04aff8 100644 --- a/x/kavadist/client/rest/rest.go +++ b/x/kavadist/client/rest/rest.go @@ -1,12 +1,48 @@ package rest import ( + "net/http" + "github.com/gorilla/mux" "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" + + "github.com/kava-labs/kava/x/kavadist/types" ) // RegisterRoutes registers kavadist-related REST handlers to a router func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { registerQueryRoutes(cliCtx, r) } + +// ProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool multi-spend REST handler with a given sub-route. +func ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: types.ProposalTypeCommunityPoolMultiSpend, + Handler: postProposalHandlerFn(cliCtx), + } +} +func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req CommunityPoolMultiSpendProposalReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + content := types.NewCommunityPoolMultiSpendProposal(req.Title, req.Description, req.RecipientList) + msg := gov.NewMsgSubmitProposal(content, req.Deposit, req.Proposer) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/kavadist/client/rest/utils.go b/x/kavadist/client/rest/utils.go new file mode 100644 index 00000000..8a0faddd --- /dev/null +++ b/x/kavadist/client/rest/utils.go @@ -0,0 +1,21 @@ +package rest + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + + "github.com/kava-labs/kava/x/kavadist/types" +) + +type ( + // CommunityPoolMultiSpendProposalReq defines a community pool multi-spend proposal request body. + CommunityPoolMultiSpendProposalReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + RecipientList types.MultiSpendRecipients `json:"recipient_list" yaml:"recipient_list"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` + } +) diff --git a/x/kavadist/handler.go b/x/kavadist/handler.go index 6d093bfd..5aa5e130 100644 --- a/x/kavadist/handler.go +++ b/x/kavadist/handler.go @@ -3,6 +3,9 @@ package kavadist import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/kava-labs/kava/x/kavadist/keeper" + "github.com/kava-labs/kava/x/kavadist/types" ) // NewHandler creates an sdk.Handler for kavadist messages @@ -15,3 +18,15 @@ func NewHandler(k Keeper) sdk.Handler { } } } + +func NewCommunityPoolMultiSpendProposalHandler(k Keeper) govtypes.Handler { + return func(ctx sdk.Context, content govtypes.Content) error { + switch c := content.(type) { + case types.CommunityPoolMultiSpendProposal: + return keeper.HandleCommunityPoolMultiSpendProposal(ctx, k, c) + + default: + return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized kavadist proposal content type: %T", c) + } + } +} diff --git a/x/kavadist/keeper/keeper.go b/x/kavadist/keeper/keeper.go index 25e9ff1c..b9c94a2e 100644 --- a/x/kavadist/keeper/keeper.go +++ b/x/kavadist/keeper/keeper.go @@ -17,19 +17,27 @@ type Keeper struct { cdc *codec.Codec paramSubspace subspace.Subspace supplyKeeper types.SupplyKeeper + distKeeper types.DistKeeper + + blacklistedAddrs map[string]bool } // NewKeeper creates a new keeper -func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper) Keeper { +func NewKeeper( + cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper, + dk types.DistKeeper, blacklistedAddrs map[string]bool, +) Keeper { if !paramstore.HasKeyTable() { paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) } return Keeper{ - key: key, - cdc: cdc, - paramSubspace: paramstore, - supplyKeeper: sk, + key: key, + cdc: cdc, + paramSubspace: paramstore, + supplyKeeper: sk, + distKeeper: dk, + blacklistedAddrs: blacklistedAddrs, } } diff --git a/x/kavadist/keeper/proposal_handler.go b/x/kavadist/keeper/proposal_handler.go new file mode 100644 index 00000000..f6ebe32c --- /dev/null +++ b/x/kavadist/keeper/proposal_handler.go @@ -0,0 +1,23 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/kava-labs/kava/x/kavadist/types" +) + +// HandleCommunityPoolMultiSpendProposal is a handler for executing a passed community multi-spend proposal +func HandleCommunityPoolMultiSpendProposal(ctx sdk.Context, k Keeper, p types.CommunityPoolMultiSpendProposal) error { + for _, receiverInfo := range p.RecipientList { + if k.blacklistedAddrs[receiverInfo.Address.String()] { + return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is blacklisted from receiving external funds", receiverInfo.Address) + } + err := k.distKeeper.DistributeFromFeePool(ctx, receiverInfo.Amount, receiverInfo.Address) + if err != nil { + return err + } + } + + return nil +} diff --git a/x/kavadist/types/codec.go b/x/kavadist/types/codec.go index 16afc367..e8321410 100644 --- a/x/kavadist/types/codec.go +++ b/x/kavadist/types/codec.go @@ -14,4 +14,5 @@ func init() { // RegisterCodec registers the necessary types for cdp module func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(CommunityPoolMultiSpendProposal{}, "kava/CommunityPoolMultiSpendProposal", nil) } diff --git a/x/kavadist/types/errors.go b/x/kavadist/types/errors.go new file mode 100644 index 00000000..5db4fb5b --- /dev/null +++ b/x/kavadist/types/errors.go @@ -0,0 +1,11 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// x/kavadist errors +var ( + ErrInvalidProposalAmount = sdkerrors.Register(ModuleName, 2, "invalid community pool multi-spend proposal amount") + ErrEmptyProposalRecipient = sdkerrors.Register(ModuleName, 3, "invalid community pool multi-spend proposal recipient") +) diff --git a/x/kavadist/types/expected_keepers.go b/x/kavadist/types/expected_keepers.go index cbbc3cef..07ed885d 100644 --- a/x/kavadist/types/expected_keepers.go +++ b/x/kavadist/types/expected_keepers.go @@ -13,3 +13,8 @@ type SupplyKeeper interface { SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error } + +// DistKeeper defines the expected distribution keeper interface +type DistKeeper interface { + DistributeFromFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) error +} diff --git a/x/kavadist/types/proposal.go b/x/kavadist/types/proposal.go new file mode 100644 index 00000000..55152e75 --- /dev/null +++ b/x/kavadist/types/proposal.go @@ -0,0 +1,120 @@ +package types + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +const ( + // ProposalTypeCommunityPoolMultiSpend defines the type for a CommunityPoolMultiSpendProposal + ProposalTypeCommunityPoolMultiSpend = "CommunityPoolMultiSpend" +) + +// Assert CommunityPoolMultiSpendProposal implements govtypes.Content at compile-time +var _ govtypes.Content = CommunityPoolMultiSpendProposal{} + +func init() { + govtypes.RegisterProposalType(ProposalTypeCommunityPoolMultiSpend) + govtypes.RegisterProposalTypeCodec(CommunityPoolMultiSpendProposal{}, "kava/CommunityPoolMultiSpendProposal") +} + +// CommunityPoolMultiSpendProposal spends from the community pool by sending to one or more addresses +type CommunityPoolMultiSpendProposal struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + RecipientList MultiSpendRecipients `json:"recipient_list" yaml:"recipient_list"` +} + +// NewCommunityPoolMultiSpendProposal creates a new community pool multi-spend proposal. +func NewCommunityPoolMultiSpendProposal(title, description string, recipientList MultiSpendRecipients) CommunityPoolMultiSpendProposal { + return CommunityPoolMultiSpendProposal{ + Title: title, + Description: description, + RecipientList: recipientList} +} + +// GetTitle returns the title of a community pool multi-spend proposal. +func (csp CommunityPoolMultiSpendProposal) GetTitle() string { return csp.Title } + +// GetDescription returns the description of a community pool multi-spend proposal. +func (csp CommunityPoolMultiSpendProposal) GetDescription() string { return csp.Description } + +// GetDescription returns the routing key of a community pool multi-spend proposal. +func (csp CommunityPoolMultiSpendProposal) ProposalRoute() string { return RouterKey } + +// ProposalType returns the type of a community pool multi-spend proposal. +func (csp CommunityPoolMultiSpendProposal) ProposalType() string { + return ProposalTypeCommunityPoolMultiSpend +} + +// ValidateBasic stateless validation of a community pool multi-spend proposal. +func (csp CommunityPoolMultiSpendProposal) ValidateBasic() error { + err := govtypes.ValidateAbstract(csp) + if err != nil { + return err + } + if err := csp.RecipientList.Validate(); err != nil { + return err + } + return nil +} + +// String implements fmt.Stringer +func (csp CommunityPoolMultiSpendProposal) String() string { + var b strings.Builder + b.WriteString(fmt.Sprintf(`Community Pool Multi Spend Proposal: + Title: %s + Description: %s + Recipient List: %s +`, csp.Title, csp.Description, csp.RecipientList)) + return b.String() +} + +// MultiSpendRecipient defines a recipient and the amount of coins they are receiving +type MultiSpendRecipient struct { + Address sdk.AccAddress `json:"address" yaml:"address"` + Amount sdk.Coins `json:"amount" yaml:"amount"` +} + +// Validate stateless validation of MultiSpendRecipient +func (msr MultiSpendRecipient) Validate() error { + if !msr.Amount.IsValid() { + return ErrInvalidProposalAmount + } + if msr.Address.Empty() { + return ErrEmptyProposalRecipient + } + return nil +} + +// String implements fmt.Stringer +func (msr MultiSpendRecipient) String() string { + return fmt.Sprintf(`Receiver: %s + Amount: %s + `, msr.Address, msr.Amount) +} + +// MultiSpendRecipients slice of MultiSpendRecipient +type MultiSpendRecipients []MultiSpendRecipient + +// Validate stateless validation of MultiSpendRecipients +func (msrs MultiSpendRecipients) Validate() error { + for _, msr := range msrs { + if err := msr.Validate(); err != nil { + return err + } + } + return nil +} + +// String implements fmt.Stringer +func (msrs MultiSpendRecipients) String() string { + out := "" + for _, msr := range msrs { + out += msr.String() + } + return out +}