github.com/Finschia/finschia-sdk@v0.49.1/x/foundation/client/cli/tx.go (about) 1 package cli 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strconv" 7 8 "github.com/spf13/cobra" 9 10 "github.com/Finschia/finschia-sdk/client" 11 "github.com/Finschia/finschia-sdk/client/flags" 12 "github.com/Finschia/finschia-sdk/client/tx" 13 "github.com/Finschia/finschia-sdk/codec" 14 sdk "github.com/Finschia/finschia-sdk/types" 15 "github.com/Finschia/finschia-sdk/x/foundation" 16 govcli "github.com/Finschia/finschia-sdk/x/gov/client/cli" 17 govtypes "github.com/Finschia/finschia-sdk/x/gov/types" 18 ) 19 20 // Proposal flags 21 const ( 22 FlagExec = "exec" 23 ExecTry = "try" 24 ) 25 26 func validateGenerateOnly(cmd *cobra.Command) error { 27 generateOnly, err := cmd.Flags().GetBool(flags.FlagGenerateOnly) 28 if err != nil { 29 return err 30 } 31 if !generateOnly { 32 return fmt.Errorf("you must use it with the flag --%s", flags.FlagGenerateOnly) 33 } 34 return nil 35 } 36 37 func parseMemberRequests(codec codec.Codec, membersJSON string) ([]foundation.MemberRequest, error) { 38 var cliMembers []json.RawMessage 39 if err := json.Unmarshal([]byte(membersJSON), &cliMembers); err != nil { 40 return nil, err 41 } 42 43 members := make([]foundation.MemberRequest, len(cliMembers)) 44 for i, cliMember := range cliMembers { 45 var member foundation.MemberRequest 46 if err := codec.UnmarshalJSON(cliMember, &member); err != nil { 47 return nil, err 48 } 49 members[i] = member 50 } 51 52 return members, nil 53 } 54 55 func parseAddresses(addressesJSON string) ([]string, error) { 56 var addresses []string 57 if err := json.Unmarshal([]byte(addressesJSON), &addresses); err != nil { 58 return nil, err 59 } 60 if len(addresses) == 0 { 61 return nil, fmt.Errorf("you must provide one address at least") 62 } 63 64 return addresses, nil 65 } 66 67 func parseDecisionPolicy(codec codec.Codec, policyJSON string) (foundation.DecisionPolicy, error) { 68 var policy foundation.DecisionPolicy 69 if err := codec.UnmarshalInterfaceJSON([]byte(policyJSON), &policy); err != nil { 70 return nil, err 71 } 72 73 return policy, nil 74 } 75 76 func parseAuthorization(codec codec.Codec, authorizationJSON string) (foundation.Authorization, error) { 77 var authorization foundation.Authorization 78 if err := codec.UnmarshalInterfaceJSON([]byte(authorizationJSON), &authorization); err != nil { 79 return nil, err 80 } 81 82 return authorization, nil 83 } 84 85 func execFromString(execStr string) foundation.Exec { 86 exec := foundation.Exec_EXEC_UNSPECIFIED 87 if execStr == ExecTry { 88 exec = foundation.Exec_EXEC_TRY 89 } 90 return exec 91 } 92 93 // VoteOptionFromString returns a VoteOption from a string. It returns an error 94 // if the string is invalid. 95 func voteOptionFromString(str string) (foundation.VoteOption, error) { 96 vo, ok := foundation.VoteOption_value[str] 97 if !ok { 98 return foundation.VOTE_OPTION_UNSPECIFIED, fmt.Errorf("'%s' is not a valid vote option", str) 99 } 100 return foundation.VoteOption(vo), nil 101 } 102 103 func parseMsgs(cdc codec.Codec, msgsJSON string) ([]sdk.Msg, error) { 104 var cliMsgs []json.RawMessage 105 if err := json.Unmarshal([]byte(msgsJSON), &cliMsgs); err != nil { 106 return nil, err 107 } 108 109 msgs := make([]sdk.Msg, len(cliMsgs)) 110 for i, anyJSON := range cliMsgs { 111 var msg sdk.Msg 112 err := cdc.UnmarshalInterfaceJSON(anyJSON, &msg) 113 if err != nil { 114 return nil, err 115 } 116 117 msgs[i] = msg 118 } 119 120 return msgs, nil 121 } 122 123 // NewTxCmd returns the transaction commands for this module 124 func NewTxCmd() *cobra.Command { 125 txCmd := &cobra.Command{ 126 Use: foundation.ModuleName, 127 Short: fmt.Sprintf("%s transactions subcommands", foundation.ModuleName), 128 DisableFlagParsing: true, 129 SuggestionsMinimumDistance: 2, 130 RunE: client.ValidateCmd, 131 } 132 133 txCmd.AddCommand( 134 NewTxCmdFundTreasury(), 135 NewTxCmdWithdrawFromTreasury(), 136 NewTxCmdUpdateMembers(), 137 NewTxCmdUpdateDecisionPolicy(), 138 NewTxCmdSubmitProposal(), 139 NewTxCmdWithdrawProposal(), 140 NewTxCmdVote(), 141 NewTxCmdExec(), 142 NewTxCmdLeaveFoundation(), 143 NewTxCmdGrant(), 144 NewTxCmdRevoke(), 145 ) 146 147 return txCmd 148 } 149 150 func NewTxCmdFundTreasury() *cobra.Command { 151 cmd := &cobra.Command{ 152 Use: "fund-treasury [from] [amount]", 153 Args: cobra.ExactArgs(2), 154 Short: "Fund the treasury", 155 Long: `Fund the treasury 156 `, 157 RunE: func(cmd *cobra.Command, args []string) error { 158 from := args[0] 159 if err := cmd.Flags().Set(flags.FlagFrom, from); err != nil { 160 return err 161 } 162 163 clientCtx, err := client.GetClientTxContext(cmd) 164 if err != nil { 165 return err 166 } 167 168 amount, err := sdk.ParseCoinsNormalized(args[1]) 169 if err != nil { 170 return err 171 } 172 173 msg := foundation.MsgFundTreasury{ 174 From: from, 175 Amount: amount, 176 } 177 if err := msg.ValidateBasic(); err != nil { 178 return err 179 } 180 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 181 }, 182 } 183 184 flags.AddTxFlagsToCmd(cmd) 185 return cmd 186 } 187 188 func NewTxCmdWithdrawFromTreasury() *cobra.Command { 189 cmd := &cobra.Command{ 190 Use: "withdraw-from-treasury [authority] [to] [amount]", 191 Args: cobra.ExactArgs(3), 192 Short: "Withdraw coins from the treasury", 193 Long: `Withdraw coins from the treasury 194 `, 195 RunE: func(cmd *cobra.Command, args []string) error { 196 if err := validateGenerateOnly(cmd); err != nil { 197 return err 198 } 199 200 clientCtx, err := client.GetClientTxContext(cmd) 201 if err != nil { 202 return err 203 } 204 205 amount, err := sdk.ParseCoinsNormalized(args[2]) 206 if err != nil { 207 return err 208 } 209 210 msg := foundation.MsgWithdrawFromTreasury{ 211 Authority: args[0], 212 To: args[1], 213 Amount: amount, 214 } 215 if err := msg.ValidateBasic(); err != nil { 216 return err 217 } 218 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 219 }, 220 } 221 222 flags.AddTxFlagsToCmd(cmd) 223 return cmd 224 } 225 226 func NewTxCmdUpdateMembers() *cobra.Command { 227 cmd := &cobra.Command{ 228 Use: "update-members [authority] [members-json]", 229 Args: cobra.ExactArgs(2), 230 Short: "Update the foundation members", 231 Long: `Update the foundation members 232 233 Example of the content of members-json: 234 235 [ 236 { 237 "address": "addr1", 238 "participating": true, 239 "metadata": "some new metadata" 240 }, 241 { 242 "address": "addr2", 243 "participating": false, 244 "metadata": "some metadata" 245 } 246 ] 247 248 Set a member's participating to false to delete it. 249 `, 250 RunE: func(cmd *cobra.Command, args []string) error { 251 if err := validateGenerateOnly(cmd); err != nil { 252 return err 253 } 254 255 clientCtx, err := client.GetClientTxContext(cmd) 256 if err != nil { 257 return err 258 } 259 260 updates, err := parseMemberRequests(clientCtx.Codec, args[1]) 261 if err != nil { 262 return err 263 } 264 265 msg := foundation.MsgUpdateMembers{ 266 Authority: args[0], 267 MemberUpdates: updates, 268 } 269 if err := msg.ValidateBasic(); err != nil { 270 return err 271 } 272 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 273 }, 274 } 275 276 flags.AddTxFlagsToCmd(cmd) 277 return cmd 278 } 279 280 func NewTxCmdUpdateDecisionPolicy() *cobra.Command { 281 cmd := &cobra.Command{ 282 Use: "update-decision-policy [authority] [policy-json]", 283 Args: cobra.ExactArgs(2), 284 Short: "Update the foundation decision policy", 285 Long: `Update the foundation decision policy 286 287 Example of the content of policy-json: 288 289 { 290 "@type": "/lbm.foundation.v1.ThresholdDecisionPolicy", 291 "threshold": "10", 292 "windows": { 293 "voting_period": "24h", 294 "min_execution_period": "0s" 295 } 296 } 297 `, 298 RunE: func(cmd *cobra.Command, args []string) error { 299 if err := validateGenerateOnly(cmd); err != nil { 300 return err 301 } 302 303 clientCtx, err := client.GetClientTxContext(cmd) 304 if err != nil { 305 return err 306 } 307 308 msg := foundation.MsgUpdateDecisionPolicy{ 309 Authority: args[0], 310 } 311 policy, err := parseDecisionPolicy(clientCtx.Codec, args[1]) 312 if err != nil { 313 return err 314 } 315 if err := msg.SetDecisionPolicy(policy); err != nil { 316 return err 317 } 318 319 if err := msg.ValidateBasic(); err != nil { 320 return err 321 } 322 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 323 }, 324 } 325 326 flags.AddTxFlagsToCmd(cmd) 327 return cmd 328 } 329 330 func NewTxCmdSubmitProposal() *cobra.Command { 331 cmd := &cobra.Command{ 332 Use: "submit-proposal [metadata] [proposers-json] [messages-json]", 333 Args: cobra.ExactArgs(3), 334 Short: "Submit a new proposal", 335 Long: `Submit a new proposal 336 337 Parameters: 338 metadata: metadata of the proposal. 339 proposers-json: the addresses of the proposers in json format. 340 messages-json: messages in json format that will be executed if the proposal is accepted. 341 342 Example of the content of proposers-json: 343 344 [ 345 "addr1", 346 "addr2" 347 ] 348 349 Example of the content of messages-json: 350 351 [ 352 { 353 "@type": "/lbm.foundation.v1.MsgWithdrawFromTreasury", 354 "authority": "addr1", 355 "to": "addr2", 356 "amount": "10000stake" 357 } 358 ] 359 `, 360 RunE: func(cmd *cobra.Command, args []string) error { 361 proposers, err := parseAddresses(args[1]) 362 if err != nil { 363 return err 364 } 365 366 signer := proposers[0] 367 if err := cmd.Flags().Set(flags.FlagFrom, signer); err != nil { 368 return err 369 } 370 371 clientCtx, err := client.GetClientTxContext(cmd) 372 if err != nil { 373 return err 374 } 375 376 messages, err := parseMsgs(clientCtx.Codec, args[2]) 377 if err != nil { 378 return err 379 } 380 381 execStr, err := cmd.Flags().GetString(FlagExec) 382 if err != nil { 383 return err 384 } 385 exec := execFromString(execStr) 386 387 msg := foundation.MsgSubmitProposal{ 388 Proposers: proposers, 389 Metadata: args[0], 390 Exec: exec, 391 } 392 if err := msg.SetMsgs(messages); err != nil { 393 return err 394 } 395 if err := msg.ValidateBasic(); err != nil { 396 return err 397 } 398 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 399 }, 400 } 401 402 flags.AddTxFlagsToCmd(cmd) 403 cmd.Flags().String(FlagExec, "", "Set to 'try' to try to execute proposal immediately after creation (proposers signatures are considered as Yes votes)") 404 405 return cmd 406 } 407 408 func NewTxCmdWithdrawProposal() *cobra.Command { 409 cmd := &cobra.Command{ 410 Use: "withdraw-proposal [proposal-id] [address]", 411 Args: cobra.ExactArgs(2), 412 Short: "Withdraw a submitted proposal", 413 Long: `Withdraw a submitted proposal. 414 415 Parameters: 416 proposal-id: unique ID of the proposal. 417 address: one of the proposer of the proposal. 418 `, 419 RunE: func(cmd *cobra.Command, args []string) error { 420 address := args[1] 421 if err := cmd.Flags().Set(flags.FlagFrom, address); err != nil { 422 return err 423 } 424 425 clientCtx, err := client.GetClientTxContext(cmd) 426 if err != nil { 427 return err 428 } 429 430 proposalID, err := strconv.ParseUint(args[0], 10, 64) 431 if err != nil { 432 return err 433 } 434 435 msg := foundation.MsgWithdrawProposal{ 436 ProposalId: proposalID, 437 Address: address, 438 } 439 if err := msg.ValidateBasic(); err != nil { 440 return err 441 } 442 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 443 }, 444 } 445 446 flags.AddTxFlagsToCmd(cmd) 447 return cmd 448 } 449 450 func NewTxCmdVote() *cobra.Command { 451 cmd := &cobra.Command{ 452 Use: "vote [proposal-id] [voter] [option] [metadata]", 453 Args: cobra.ExactArgs(4), 454 Short: "Vote on a proposal", 455 Long: `Vote on a proposal. 456 457 Parameters: 458 proposal-id: unique ID of the proposal 459 voter: voter account addresses. 460 vote-option: choice of the voter(s) 461 VOTE_OPTION_UNSPECIFIED: no-op 462 VOTE_OPTION_NO: no 463 VOTE_OPTION_YES: yes 464 VOTE_OPTION_ABSTAIN: abstain 465 VOTE_OPTION_NO_WITH_VETO: no-with-veto 466 metadata: metadata for the vote 467 `, 468 RunE: func(cmd *cobra.Command, args []string) error { 469 voter := args[1] 470 if err := cmd.Flags().Set(flags.FlagFrom, voter); err != nil { 471 return err 472 } 473 474 clientCtx, err := client.GetClientTxContext(cmd) 475 if err != nil { 476 return err 477 } 478 479 proposalID, err := strconv.ParseUint(args[0], 10, 64) 480 if err != nil { 481 return err 482 } 483 484 option, err := voteOptionFromString(args[2]) 485 if err != nil { 486 return err 487 } 488 489 execStr, err := cmd.Flags().GetString(FlagExec) 490 if err != nil { 491 return err 492 } 493 exec := execFromString(execStr) 494 495 msg := foundation.MsgVote{ 496 ProposalId: proposalID, 497 Voter: voter, 498 Option: option, 499 Metadata: args[3], 500 Exec: exec, 501 } 502 if err := msg.ValidateBasic(); err != nil { 503 return err 504 } 505 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 506 }, 507 } 508 509 flags.AddTxFlagsToCmd(cmd) 510 cmd.Flags().String(FlagExec, "", "Set to 'try' to try to execute proposal immediately after voting") 511 512 return cmd 513 } 514 515 func NewTxCmdExec() *cobra.Command { 516 cmd := &cobra.Command{ 517 Use: "exec [proposal-id] [signer]", 518 Args: cobra.ExactArgs(2), 519 Short: "Execute a proposal", 520 RunE: func(cmd *cobra.Command, args []string) error { 521 signer := args[1] 522 if err := cmd.Flags().Set(flags.FlagFrom, signer); err != nil { 523 return err 524 } 525 526 clientCtx, err := client.GetClientTxContext(cmd) 527 if err != nil { 528 return err 529 } 530 531 proposalID, err := strconv.ParseUint(args[0], 10, 64) 532 if err != nil { 533 return err 534 } 535 536 msg := foundation.MsgExec{ 537 ProposalId: proposalID, 538 Signer: signer, 539 } 540 if err := msg.ValidateBasic(); err != nil { 541 return err 542 } 543 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 544 }, 545 } 546 547 flags.AddTxFlagsToCmd(cmd) 548 return cmd 549 } 550 551 func NewTxCmdLeaveFoundation() *cobra.Command { 552 cmd := &cobra.Command{ 553 Use: "leave-foundation [address]", 554 Args: cobra.ExactArgs(1), 555 Short: "Leave the foundation", 556 RunE: func(cmd *cobra.Command, args []string) error { 557 address := args[0] 558 if err := cmd.Flags().Set(flags.FlagFrom, address); err != nil { 559 return err 560 } 561 562 clientCtx, err := client.GetClientTxContext(cmd) 563 if err != nil { 564 return err 565 } 566 567 msg := foundation.MsgLeaveFoundation{ 568 Address: address, 569 } 570 if err := msg.ValidateBasic(); err != nil { 571 return err 572 } 573 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 574 }, 575 } 576 577 flags.AddTxFlagsToCmd(cmd) 578 return cmd 579 } 580 581 func NewTxCmdGrant() *cobra.Command { 582 cmd := &cobra.Command{ 583 Use: "grant [authority] [grantee] [authorization-json]", 584 Args: cobra.ExactArgs(3), 585 Short: "Grant an authorization to grantee", 586 Long: `Grant an authorization to grantee 587 588 Example of the content of authorization-json: 589 590 { 591 "@type": "/lbm.foundation.v1.ReceiveFromTreasuryAuthorization", 592 "receive_limit": [ 593 "denom": "stake", 594 "amount": "10000" 595 ] 596 } 597 `, 598 RunE: func(cmd *cobra.Command, args []string) error { 599 if err := validateGenerateOnly(cmd); err != nil { 600 return err 601 } 602 603 clientCtx, err := client.GetClientTxContext(cmd) 604 if err != nil { 605 return err 606 } 607 608 msg := foundation.MsgGrant{ 609 Authority: args[0], 610 Grantee: args[1], 611 } 612 authorization, err := parseAuthorization(clientCtx.Codec, args[2]) 613 if err != nil { 614 return err 615 } 616 if err := msg.SetAuthorization(authorization); err != nil { 617 return err 618 } 619 620 if err := msg.ValidateBasic(); err != nil { 621 return err 622 } 623 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 624 }, 625 } 626 627 flags.AddTxFlagsToCmd(cmd) 628 return cmd 629 } 630 631 func NewTxCmdRevoke() *cobra.Command { 632 cmd := &cobra.Command{ 633 Use: "revoke [authority] [grantee] [msg-type-url]", 634 Args: cobra.ExactArgs(3), 635 Short: "Revoke an authorization of grantee", 636 Long: `Revoke an authorization of grantee 637 `, 638 RunE: func(cmd *cobra.Command, args []string) error { 639 if err := validateGenerateOnly(cmd); err != nil { 640 return err 641 } 642 643 clientCtx, err := client.GetClientTxContext(cmd) 644 if err != nil { 645 return err 646 } 647 648 msg := foundation.MsgRevoke{ 649 Authority: args[0], 650 Grantee: args[1], 651 MsgTypeUrl: args[2], 652 } 653 if err := msg.ValidateBasic(); err != nil { 654 return err 655 } 656 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) 657 }, 658 } 659 660 flags.AddTxFlagsToCmd(cmd) 661 return cmd 662 } 663 664 // NewProposalCmdFoundationExecProposal returns a CLI command handler for 665 // creating a foundation exec proposal governance transaction. 666 func NewProposalCmdFoundationExec() *cobra.Command { 667 cmd := &cobra.Command{ 668 Use: "foundation-exec [messages-json]", 669 Args: cobra.ExactArgs(1), 670 Short: "Submit a foundation exec proposal", 671 Long: ` 672 Parameters: 673 messages-json: messages in json format that will be executed if the proposal is accepted. 674 675 Example of the content of messages-json: 676 677 [ 678 { 679 "@type": "/lbm.foundation.v1.MsgUpdateCensorship", 680 "authority": "addr1", 681 "censorship": { 682 "msg_type_url": "/cosmos.staking.v1beta1.MsgCreateValidator", 683 "authority": "CENSORSHIP_AUTHORITY_UNSPECIFIED" 684 } 685 } 686 ] 687 `, 688 RunE: func(cmd *cobra.Command, args []string) error { 689 clientCtx, err := client.GetClientTxContext(cmd) 690 if err != nil { 691 return err 692 } 693 694 from := clientCtx.GetFromAddress() 695 696 title, err := cmd.Flags().GetString(govcli.FlagTitle) 697 if err != nil { 698 return err 699 } 700 701 description, err := cmd.Flags().GetString(govcli.FlagDescription) 702 if err != nil { 703 return err 704 } 705 706 messages, err := parseMsgs(clientCtx.Codec, args[0]) 707 if err != nil { 708 return err 709 } 710 711 content := foundation.NewFoundationExecProposal(title, description, messages) 712 713 depositStr, err := cmd.Flags().GetString(govcli.FlagDeposit) 714 if err != nil { 715 return err 716 } 717 deposit, err := sdk.ParseCoinsNormalized(depositStr) 718 if err != nil { 719 return err 720 } 721 722 msg, err := govtypes.NewMsgSubmitProposal(content, deposit, from) 723 if err != nil { 724 return err 725 } 726 if err := msg.ValidateBasic(); err != nil { 727 return err 728 } 729 730 return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) 731 }, 732 } 733 734 cmd.Flags().String(govcli.FlagTitle, "", "title of proposal") 735 cmd.Flags().String(govcli.FlagDescription, "", "description of proposal") 736 cmd.Flags().String(govcli.FlagDeposit, "", "deposit of proposal") 737 738 return cmd 739 }